Защитите свои данные 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

Затем перейдите в директорию initial-state, где вы будете работать до конца этого практического занятия:

$ 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. Запустите тесты

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

Запустите эмуляторы

Приложение, с которым вы будете работать, имеет три основные коллекции 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, match /{document=**} , использует синтаксис ** для рекурсивного применения ко всем документам в подколлекциях. И поскольку он находится на верхнем уровне, сейчас одно и то же общее правило применяется ко всем запросам, независимо от того, кто отправляет запрос и какие данные он пытается прочитать или записать.

Для начала удалите самый внутренний оператор match и замените его на 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 лица, подающего запрос, совпадает с 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. Обновление опубликованных записей: пользовательские функции и локальные переменные

Условия для обновления опубликованной записи следующие:

  • Это может сделать только автор или модератор, и
  • Оно должно содержать все необходимые поля.

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

Создайте пользовательскую функцию

Выше оператора match для черновиков создайте новую функцию с именем 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:

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