1. Прежде чем начать
Cloud Firestore, Cloud Storage for Firebase и Realtime Database используют файлы конфигурации, которые вы пишете для предоставления доступа на чтение и запись. Эта конфигурация, называемая правилами безопасности, также может служить своего рода схемой для вашего приложения. Это один из важнейших этапов разработки приложения. И эта лабораторная работа поможет вам разобраться в этом.
Предпосылки
- Простой редактор, такой как Visual Studio Code, Atom или Sublime Text
- Node.js 8.6.0 или выше (чтобы установить Node.js, используйте nvm ; чтобы проверить версию, выполните
node --version
) - Java 7 или выше (для установки Java следуйте этим инструкциям ; чтобы проверить свою версию, выполните
java -version
)
Что ты будешь делать?
В этой лабораторной работе вы создадите простую блог-платформу на базе Firestore. Вы будете использовать эмулятор Firestore для запуска модульных тестов на соответствие правилам безопасности и убедитесь, что правила разрешают или запрещают доступ, который вы ожидаете.
Вы узнаете, как:
- Предоставьте детальные разрешения
- Обеспечить проверку данных и типов
- Реализовать контроль доступа на основе атрибутов
- Предоставить доступ на основе метода аутентификации
- Создание пользовательских функций
- Создание правил безопасности на основе времени
- Реализуйте черный список и мягкое удаление
- Понимание того, когда следует денормализовать данные для соответствия шаблонам множественного доступа.
2. Настройка
Это приложение для ведения блога. Вот краткий обзор его функциональности:
Черновики сообщений в блоге:
- Пользователи могут создавать черновики сообщений в блоге, которые хранятся в коллекции
drafts
. - Автор может продолжать обновлять черновик до тех пор, пока он не будет готов к публикации.
- Когда документ готов к публикации, запускается функция Firebase, которая создает новый документ в
published
коллекции. - Черновики могут быть удалены автором или модераторами сайта.
Опубликованные записи в блоге:
- Опубликованные сообщения не могут быть созданы пользователями, только с помощью функции.
- Их можно только удалить «обратно», при этом
visible
атрибут обновляется до значения false.
Комментарии
- Опубликованные посты допускают комментарии, которые представляют собой подколлекцию каждого опубликованного поста.
- Чтобы сократить количество злоупотреблений, пользователи должны иметь подтвержденный адрес электронной почты и не быть в списке «отклоняющих», чтобы оставить комментарий.
- Комментарии можно обновлять только в течение часа после публикации.
- Комментарии могут быть удалены автором комментария, автором исходного сообщения или модераторами.
Помимо правил доступа вы создадите правила безопасности, обеспечивающие заполнение обязательных полей и проверку данных.
Все будет происходить локально, с использованием Firebase Emulator Suite.
Получить исходный код
В этой лабораторной работе вы начнете с тестов для правил безопасности, но самих правил безопасности будет минимум, поэтому первое, что вам нужно сделать, — это клонировать исходный код для запуска тестов:
$ git clone https://github.com/FirebaseExtended/codelab-rules.git
Затем перейдите в каталог начального состояния, где вы будете работать над оставшейся частью этой лабораторной работы:
$ cd codelab-rules/initial-state
Теперь установите зависимости, чтобы можно было запустить тесты. Если у вас медленное интернет-соединение, это может занять минуту или две:
# Move into the functions directory, install dependencies, jump out. $ cd functions && npm install && cd -
Получить Firebase CLI
Набор эмуляторов, который вы будете использовать для запуска тестов, является частью Firebase CLI (интерфейса командной строки), который можно установить на ваш компьютер с помощью следующей команды:
$ npm install -g firebase-tools
Затем убедитесь, что у вас установлена последняя версия CLI. Эта лабораторная работа должна работать с версией 8.4.0 или выше, но в более поздних версиях исправлено больше ошибок.
$ firebase --version 9.10.2
3. Проведите тесты
В этом разделе вы запустите тесты локально. Это значит, что пришло время запустить Emulator Suite.
Запустить эмуляторы
Приложение, с которым вы будете работать, содержит три основные коллекции Firestore: drafts
содержат записи блога, находящиеся в процессе разработки, published
коллекция содержит опубликованные записи блога, а comments
— подколлекция опубликованных записей. Репозиторий содержит модульные тесты для правил безопасности, которые определяют атрибуты пользователя и другие условия, необходимые для создания, чтения, обновления и удаления документов в коллекциях drafts
, published
и comments
. Вам нужно будет написать правила безопасности, которые обеспечат прохождение этих тестов.
Для начала ваша база данных заблокирована: чтение и запись в неё запрещены, и все тесты не пройдены. По мере написания правил безопасности тесты будут проходить успешно. Чтобы увидеть тесты, откройте functions/test.js
в редакторе.
В командной строке запустите эмуляторы с помощью emulators:exec
и выполните тесты:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test"
Прокрутите к началу вывода:
$ firebase emulators:exec --project=codelab --import=.seed "cd functions; npm test" i emulators: Starting emulators: functions, firestore, hosting ⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, pubsub ⚠ functions: Unable to fetch project Admin SDK configuration, Admin SDK behavior in Cloud Functions emulator may be incorrect. i firestore: Importing data from /Users/user/src/firebase/rules-codelab/initial-state/.seed/firestore_export/firestore_export.overall_export_metadata i firestore: Firestore Emulator logging to firestore-debug.log ⚠ hosting: Authentication error when trying to fetch your current web app configuration, have you run firebase login? ⚠ hosting: Could not fetch web app configuration and there is no cached configuration on this machine. Check your internet connection and make sure you are authenticated. To continue, you must call firebase.initializeApp({...}) in your code before using Firebase. i hosting: Serving hosting files from: public ✔ hosting: Local server: http://localhost:5000 i functions: Watching "/Users/user/src/firebase/rules-codelab/initial-state/functions" for Cloud Functions... ✔ functions[publishPost]: http function initialized (http://localhost:5001/codelab/us-central1/publishPost). ✔ functions[softDelete]: http function initialized (http://localhost:5001/codelab/us-central1/softDelete). i Running script: pushd functions; npm test ~/src/firebase/rules-codelab/initial-state/functions ~/src/firebase/rules-codelab/initial-state > functions@ test /Users/user/src/firebase/rules-codelab/initial-state/functions > mocha (node:76619) ExperimentalWarning: Conditional exports is an experimental feature. This feature could change at any time Draft blog posts 1) can be created with required fields by the author 2) can be updated by author if immutable fields are unchanged 3) can be read by the author and moderator Published blog posts 4) can be read by everyone; created or deleted by no one 5) can be updated by author or moderator Comments on published blog posts 6) can be read by anyone with a permanent account 7) can be created if email is verfied and not blocked 8) can be updated by author for 1 hour after creation 9) can be deleted by an author or moderator 0 passing (848ms) 9 failing ...
Сейчас зафиксировано 9 сбоев. По мере создания файла правил вы можете отслеживать прогресс, наблюдая за прохождением большего количества тестов.
4. Создавайте черновики записей в блоге.
Поскольку доступ к черновикам записей блога сильно отличается от доступа к опубликованным записям, это приложение для ведения блогов хранит черновики записей блога в отдельной коллекции /drafts
. Доступ к черновикам есть только у автора или модератора, и предусмотрена проверка обязательных и неизменяемых полей.
Открыв файл firestore.rules
, вы найдете файл правил по умолчанию:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
Оператор сопоставления, match /{document=**}
, использует синтаксис **
для рекурсивного применения ко всем документам во вложенных коллекциях. И поскольку он находится на верхнем уровне, сейчас ко всем запросам применяется одно и то же общее правило, независимо от того, кто его отправляет и какие данные он пытается прочитать или записать.
Начните с удаления самого внутреннего оператора сопоставления и замены его на match /drafts/{draftID}
. (Комментарии о структуре документов могут быть полезны в правилах и будут включены в эту практическую работу; они всегда необязательны.)
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
}
}
}
Первое правило, которое вы напишете для черновиков, будет определять, кто может создавать документы. В этом приложении черновики может создавать только тот, кто указан в качестве автора. Убедитесь, что уникальный идентификатор (UID) пользователя, отправившего запрос, совпадает с идентификатором, указанным в документе.
Первым условием создания будет:
request.resource.data.authorUID == request.auth.uid
Далее, документы могут быть созданы только в том случае, если они содержат три обязательных поля: authorUID
, createdAt
и title
. (Пользователь не указывает поле createdAt
; это означает, что приложение должно добавить его перед попыткой создания документа.) Поскольку вам нужно только проверить, создаются ли атрибуты, вы можете проверить, что request.resource
содержит все эти ключи:
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
])
Последнее требование к созданию записи в блоге — заголовок не может быть длиннее 50 символов:
request.resource.data.title.size() < 50
Поскольку все эти условия должны быть истинными, объедините их логическим оператором И, &&
. Первое правило принимает вид:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
В терминале повторно запустите тесты и убедитесь, что первый тест пройден.
5. Обновляйте черновики записей в блоге.
Далее, по мере того, как авторы дорабатывают черновики своих записей в блоге, они будут редактировать черновики документов. Создайте правило для условий, при которых запись может быть обновлена. Во-первых, обновлять свои черновики может только автор. Обратите внимание, что здесь проверяется уже записанный UID, resource.data.authorUID
:
resource.data.authorUID == request.auth.uid
Второе требование к обновлению заключается в том, что два атрибута, authorUID
и createdAt
не должны изменяться:
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]);
И наконец, заголовок должен быть длиной не более 50 символов:
request.resource.data.title.size() < 50;
Поскольку все эти условия должны быть соблюдены, объедините их вместе с помощью &&
:
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
Полные правила выглядят так:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
}
}
}
Повторите тесты и убедитесь, что еще один тест пройден.
6. Удаление и чтение черновиков: контроль доступа на основе атрибутов
Авторы могут не только создавать и обновлять черновики, но и удалять их.
resource.data.authorUID == request.auth.uid
Кроме того, авторам с атрибутом isModerator
в токене авторизации разрешено удалять черновики:
request.auth.token.isModerator == true
Поскольку любого из этих условий достаточно для удаления, объедините их с помощью логического оператора ИЛИ, ||
:
allow delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Те же условия применяются к чтению, поэтому разрешение можно добавить в правило:
allow read, delete: if resource.data.authorUID == request.auth.uid || request.auth.token.isModerator == true
Полные правила теперь таковы:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
}
}
Повторите тесты и убедитесь, что еще один тест теперь пройден.
7. Чтение, создание и удаление опубликованных сообщений: денормализация для различных шаблонов доступа
Поскольку шаблоны доступа к опубликованным и черновикам записей сильно различаются, это приложение денормализует записи, разделяя их на коллекции draft
и published
записей. Например, опубликованные записи могут быть прочитаны любым пользователем, но не могут быть удалены без возможности восстановления, в то время как черновики могут быть удалены, но читать их могут только автор и модераторы. В этом приложении, когда пользователь хочет опубликовать черновик записи в блоге, активируется функция, которая создаёт новую опубликованную запись.
Далее вам нужно написать правила для опубликованных сообщений. Самое простое правило заключается в том, что опубликованные сообщения могут быть прочитаны любым пользователем и не могут быть созданы или удалены никем. Добавьте следующие правила:
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
Добавив их к существующим правилам, весь файл правил станет следующим:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
// Title must be < 50 characters long
request.resource.data.title.size() < 50;
allow read, delete: if
// User is draft author
resource.data.authorUID == request.auth.uid ||
// User is a moderator
request.auth.token.isModerator == true;
}
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
}
}
}
Повторите тесты и убедитесь, что еще один тест пройден.
8. Обновление опубликованных сообщений: пользовательские функции и локальные переменные
Условия обновления опубликованного поста:
- это может сделать только автор или модератор, и
- он должен содержать все обязательные поля.
Поскольку вы уже написали условия для авторов и модераторов, вы можете скопировать их и вставить, но со временем это может стать сложно читать и поддерживать. Вместо этого вы создадите пользовательскую функцию, которая инкапсулирует логику для авторов и модераторов. Затем вы будете вызывать её из нескольких условий.
Создать пользовательскую функцию
Над оператором сопоставления для черновиков создайте новую функцию с именем isAuthorOrModerator
, которая принимает в качестве аргументов документ записи (это будет работать как для черновиков, так и для опубликованных записей) и объект аутентификации пользователя:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
}
match /drafts/{postID} {
allow create: ...
allow update: ...
...
}
match /published/{postID} {
allow read: ...
allow create, delete: ...
}
}
}
Использовать локальные переменные
Внутри функции используйте ключевое слово let
для установки переменных isAuthor
и isModerator
. Все функции должны заканчиваться оператором return, а наша функция вернет логическое значение, указывающее, является ли хотя бы одна из переменных истинной:
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
Вызов функции
Теперь обновите правило для черновиков, чтобы они вызывали эту функцию, не забыв передать resource.data
в качестве первого аргумента:
// Draft blog posts
match /drafts/{draftID} {
...
// Can be deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
Теперь вы можете написать условие для обновления опубликованных постов, которое также будет использовать новую функцию:
allow update: if isAuthorOrModerator(resource.data, request.auth);
Добавить проверки
Некоторые поля опубликованной записи не следует изменять, в частности, поля url
, authorUID
и publishedAt
являются неизменяемыми. Два других поля, title
и content
, а также visible
, должны присутствовать после обновления. Добавьте условия, чтобы обеспечить соблюдение этих требований при обновлении опубликованных записей:
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
])
Создайте собственную функцию самостоятельно
И наконец, добавьте условие, чтобы заголовок не превышал 50 символов. Поскольку это повторно используемая логика, можно создать новую функцию titleIsUnder50Chars
. С новой функцией условие обновления опубликованной записи становится следующим:
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
Полный файл правил выглядит так:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User creating document is draft author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and url fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is the author, and
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
}
}
Перепройдите тесты. На этом этапе у вас должно быть 5 пройденных тестов и 4 проваленных.
9. Комментарии: Подколлекции и разрешения поставщика входа
Опубликованные записи допускают комментарии, которые хранятся в подколлекции опубликованной записи ( /published/{postID}/comments/{commentID}
). По умолчанию правила коллекции не применяются к подколлекциям. Вам не нужно, чтобы те же правила, что и для родительского документа опубликованной записи, применялись к комментариям; вы создадите другие правила.
Чтобы написать правила доступа к комментариям, начните с оператора сопоставления:
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `comment`: string, < 500 characters, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
Чтение комментариев: Невозможно быть анонимным
В этом приложении комментарии могут читать только пользователи, создавшие постоянную, а не анонимную учётную запись. Чтобы реализовать это правило, проверьте атрибут sign_in_provider
в каждом объекте auth.token
:
allow read: if request.auth.token.firebase.sign_in_provider != "anonymous";
Повторите тесты и убедитесь, что еще один тест пройден.
Создание комментариев: проверка списка заблокированных комментариев
Для создания комментария необходимо выполнить три условия:
- пользователь должен иметь подтвержденный адрес электронной почты
- комментарий должен быть короче 500 символов, и
- Они не могут быть в списке забаненных пользователей, который хранится в Firestore в коллекции
bannedUsers
. Рассмотрим эти условия по одному:
request.auth.token.email_verified == true
request.resource.data.comment.size() < 500
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
Последнее правило создания комментариев:
allow create: if
// User has verified email
(request.auth.token.email_verified == true) &&
// UID is not on bannedUsers list
!(exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
Полный файл правил теперь выглядит так:
For bottom of step 9
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
}
}
}
Повторите тесты и убедитесь, что еще один тест пройден.
10. Обновление комментариев: правила, основанные на времени
Бизнес-логика комментариев заключается в том, что автор может редактировать их в течение часа после создания. Для этого используется метка времени createdAt
.
Во-первых, чтобы установить, что пользователь является автором:
request.auth.uid == resource.data.authorUID
Далее, комментарий был создан в течение последнего часа:
(request.time - resource.data.createdAt) < duration.value(1, 'h');
Объединяя их с логическим оператором И, правило обновления комментариев становится следующим:
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
Повторите тесты и убедитесь, что еще один тест пройден.
11. Удаление комментариев: проверка родительского права собственности
Комментарии могут быть удалены автором комментария, модератором или автором записи в блоге.
Во-первых, поскольку вспомогательная функция, которую вы добавили ранее, проверяет поле authorUID
, которое может существовать как в посте, так и в комментарии, вы можете повторно использовать вспомогательную функцию для проверки того, является ли пользователь автором или модератором:
isAuthorOrModerator(resource.data, request.auth)
Чтобы проверить, является ли пользователь автором записи в блоге, используйте get
для поиска записи в Firestore:
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID
Поскольку любое из этих условий достаточно, используйте между ними логический оператор ИЛИ:
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
Повторите тесты и убедитесь, что еще один тест пройден.
А весь файл правил такой:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Returns true if user is post author or a moderator
function isAuthorOrModerator(post, auth) {
let isAuthor = auth.uid == post.authorUID;
let isModerator = auth.token.isModerator == true;
return isAuthor || isModerator;
}
function titleIsUnder50Chars(post) {
return post.title.size() < 50;
}
// Draft blog posts
match /drafts/{draftID} {
// `authorUID`: string, required
// `content`: string, optional
// `createdAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, optional
allow create: if
// User is author
request.auth.uid == request.resource.data.authorUID &&
// Must include title, author, and createdAt fields
request.resource.data.keys().hasAll([
"authorUID",
"createdAt",
"title"
]) &&
titleIsUnder50Chars(request.resource.data);
allow update: if
// User is author
resource.data.authorUID == request.auth.uid &&
// `authorUID` and `createdAt` are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"createdAt"
]) &&
titleIsUnder50Chars(request.resource.data);
// Can be read or deleted by author or moderator
allow read, delete: if isAuthorOrModerator(resource.data, request.auth);
}
// Published blog posts are denormalized from drafts
match /published/{postID} {
// `authorUID`: string, required
// `content`: string, required
// `publishedAt`: timestamp, required
// `title`: string, < 50 characters, required
// `url`: string, required
// `visible`: boolean, required
// Can be read by everyone
allow read: if true;
// Published posts are created only via functions, never by users
// No hard deletes; soft deletes update `visible` field.
allow create, delete: if false;
allow update: if
isAuthorOrModerator(resource.data, request.auth) &&
// Immutable fields are unchanged
request.resource.data.diff(resource.data).unchangedKeys().hasAll([
"authorUID",
"publishedAt",
"url"
]) &&
// Required fields are present
request.resource.data.keys().hasAll([
"content",
"title",
"visible"
]) &&
titleIsUnder50Chars(request.resource.data);
}
match /published/{postID}/comments/{commentID} {
// `authorUID`: string, required
// `createdAt`: timestamp, required
// `editedAt`: timestamp, optional
// `comment`: string, < 500 characters, required
// Must have permanent account to read comments
allow read: if !(request.auth.token.firebase.sign_in_provider == "anonymous");
allow create: if
// User has verified email
request.auth.token.email_verified == true &&
// Comment is under 500 characters
request.resource.data.comment.size() < 500 &&
// UID is not on the block list
!exists(/databases/$(database)/documents/bannedUsers/$(request.auth.uid));
allow update: if
// is author
request.auth.uid == resource.data.authorUID &&
// within an hour of comment creation
(request.time - resource.data.createdAt) < duration.value(1, 'h');
allow delete: if
// is comment author or moderator
isAuthorOrModerator(resource.data, request.auth) ||
// is blog post author
request.auth.uid == get(/databases/$(database)/documents/published/$(postID)).data.authorUID;
}
}
}
12. Дальнейшие шаги
Поздравляем! Вы написали правила безопасности, которые позволили пройти все тесты и защитить приложение!
Вот несколько тем, которые стоит рассмотреть подробнее:
- Запись в блоге : Как проверять код. Правила безопасности.
- Codelab : прохождение первой локальной разработки с использованием эмуляторов
- Видео : как настроить CI для тестов на основе эмулятора с помощью GitHub Actions