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

1. Прежде чем начать

Cloud Firestore, Cloud Storage for Firebase и база данных Realtime полагаются на файлы конфигурации, которые вы пишете, чтобы предоставить доступ для чтения и записи. Эта конфигурация, называемая правилами безопасности, также может действовать как своего рода схема вашего приложения. Это одна из наиболее важных частей разработки вашего приложения. И эта кодовая лаборатория проведет вас через это.

Предварительные условия

  • Простой редактор, такой как 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

Пакет эмулятора, который вы будете использовать для запуска тестов, является частью 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 лица, делающего запрос, совпадает с 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

Поскольку все эти условия должны быть истинными, объедините их вместе с помощью логического оператора AND, && . Первое правило звучит так:

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:

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