Защитите свои данные Firestore с помощью правил безопасности Firebase

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