Безопасный запрос данных

Эта страница основана на концепциях, изложенных в разделах «Структурирование правил безопасности» и «Написание условий для правил безопасности» , и объясняет, как Cloud Firestore Security Rules взаимодействуют с запросами. Здесь более подробно рассматривается, как правила безопасности влияют на создаваемые вами запросы, и описывается, как обеспечить использование в запросах тех же ограничений, что и в правилах безопасности. На этой странице также описывается, как создавать правила безопасности, разрешающие или запрещающие запросы на основе таких свойств запроса, как limit и orderBy .

Правила не являются фильтрами

При составлении запросов для извлечения документов помните, что правила безопасности не являются фильтрами — запросы работают по принципу «всё или ничего». Чтобы сэкономить ваше время и ресурсы, Cloud Firestore оценивает запрос по его потенциальному набору результатов, а не по фактическим значениям полей для всех ваших документов. Если запрос потенциально может вернуть документы, на чтение которых у клиента нет прав, весь запрос будет отклонен.

Запросы и правила безопасности

Как показывают приведенные ниже примеры, вы должны писать свои запросы так, чтобы они соответствовали ограничениям ваших правил безопасности.

Защита и запрос документов на основе auth.uid

В следующем примере показано, как написать запрос для извлечения документов, защищённых правилом безопасности. Рассмотрим базу данных, содержащую коллекцию документов story :

/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time...",
  author: "some_auth_id",
  published: false
}

Помимо полей title и content , каждый документ хранит поля author и published , которые используются для управления доступом. В этих примерах предполагается, что приложение использует аутентификацию Firebase для установки в поле « author UID пользователя, создавшего документ. Аутентификация Firebase также заполняет переменную request.auth в правилах безопасности.

Следующее правило безопасности использует переменные request.auth и resource.data для ограничения доступа на чтение и запись для каждой story ее автором:

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Only the authenticated user who authored the document can read or write
      allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

Предположим, что ваше приложение включает страницу, на которой пользователю отображается список документов- story , созданных им. Вы могли бы ожидать, что для заполнения этой страницы можно использовать следующий запрос. Однако этот запрос завершится ошибкой, поскольку он не содержит тех же ограничений, что и ваши правила безопасности:

Недопустимо : ограничения запроса не соответствуют ограничениям правил безопасности.

// This query will fail
db.collection("stories").get()

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

Напротив, следующий запрос выполняется успешно, поскольку он включает в себя то же ограничение на поле author , что и правила безопасности:

Допустимо : ограничения запроса соответствуют ограничениям правил безопасности.

var user = firebase.auth().currentUser;

db.collection("stories").where("author", "==", user.uid).get()

Защита и запрос документов на основе поля

Чтобы дополнительно продемонстрировать взаимодействие между запросами и правилами, приведенные ниже правила безопасности расширяют доступ на чтение для коллекции stories , позволяя любому пользователю читать документы story , в которых published поле имеет значение true .

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Anyone can read a published story; only story authors can read unpublished stories
      allow read: if resource.data.published == true || (request.auth != null && request.auth.uid == resource.data.author);
      // Only story authors can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

Запрос на опубликованные страницы должен включать те же ограничения, что и правила безопасности:

db.collection("stories").where("published", "==", true).get()

Ограничение запроса .where("published", "==", true) гарантирует, что resource.data.published будет true для любого результата. Следовательно, этот запрос удовлетворяет правилам безопасности и может читать данные.

OR запросы

При оценке логического запроса OR ( or , in , or array-contains-any ) по набору правил Cloud Firestore оценивает каждое значение сравнения отдельно. Каждое значение сравнения должно соответствовать ограничениям правила безопасности. Например, для следующего правила:

match /mydocuments/{doc} {
  allow read: if resource.data.x > 5;
}

Неверно : запрос не гарантирует, что x > 5 для всех потенциальных документов.

// These queries will fail
query(db.collection("mydocuments"),
      or(where("x", "==", 1),
         where("x", "==", 6)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [1, 3, 6, 42, 99])
    )

Действительный : запрос гарантирует, что x > 5 для всех потенциальных документов.

query(db.collection("mydocuments"),
      or(where("x", "==", 6),
         where("x", "==", 42)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [6, 42, 99, 105, 200])
    )

Оценка ограничений запросов

Ваши правила безопасности также могут принимать или отклонять запросы в зависимости от установленных ограничений. Переменная request.query содержит свойства запроса limit , offset и orderBy . Например, ваши правила безопасности могут отклонять любой запрос, который не ограничивает максимальное количество извлекаемых документов определённым диапазоном:

allow list: if request.query.limit <= 10;

Следующий набор правил демонстрирует, как писать правила безопасности, оценивающие ограничения, накладываемые на запросы. Этот пример расширяет набор правил из предыдущей stories следующими изменениями:

  • Набор правил разделяет правило чтения на правила для get и list .
  • Правило get ограничивает извлечение отдельных документов общедоступными документами или документами, созданными пользователем.
  • Правило list применяет те же ограничения, что и get , но для запросов. Оно также проверяет лимит запросов, а затем отклоняет любые запросы без лимита или с лимитом больше 10.
  • Набор правил определяет функцию authorOrPublished() чтобы избежать дублирования кода.
service cloud.firestore {

  match /databases/{database}/documents {

    match /stories/{storyid} {

      // Returns `true` if the requested story is 'published'
      // or the user authored the story
      function authorOrPublished() {
        return resource.data.published == true || request.auth.uid == resource.data.author;
      }

      // Deny any query not limited to 10 or fewer documents
      // Anyone can query published stories
      // Authors can query their unpublished stories
      allow list: if request.query.limit <= 10 &&
                     authorOrPublished();

      // Anyone can retrieve a published story
      // Only a story's author can retrieve an unpublished story
      allow get: if authorOrPublished();

      // Only a story's author can write to a story
      allow write: if request.auth.uid == resource.data.author;
    }

  }
}

Запросы групп сбора и правила безопасности

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

Защита и запрос документов на основе групп коллекций

В правилах безопасности необходимо явно разрешить запросы группы коллекций, написав правило для группы коллекций:

  1. Убедитесь, что rules_version = '2'; — это первая строка вашего набора правил. Для запросов к группам коллекций требуется новое рекурсивное поведение подстановочных знаков {name=**} правил безопасности версии 2.
  2. Напишите правило для вашей группы коллекций, используя match /{path=**}/ [COLLECTION_ID] /{doc} .

Например, рассмотрим форум, организованный в виде документов forum , содержащих подколлекции posts :

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
}

В этом приложении мы делаем публикации доступными для редактирования их владельцами и для чтения аутентифицированными пользователями:

service cloud.firestore {
  match /databases/{database}/documents {
    match /forums/{forumid}/posts/{post} {
      // Only authenticated users can read
      allow read: if request.auth != null;
      // Only the post author can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

Любой аутентифицированный пользователь может получить доступ к сообщениям любого форума:

db.collection("forums/technology/posts").get()

Но что, если вы хотите показать текущему пользователю его сообщения на всех форумах? Вы можете использовать групповой запрос для получения результатов из всех коллекций posts :

var user = firebase.auth().currentUser;

db.collectionGroup("posts").where("author", "==", user.uid).get()

В правилах безопасности необходимо разрешить этот запрос, написав правило чтения или перечисления для группы сбора posts :

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {
    // Authenticated users can query the posts collection group
    // Applies to collection queries, collection group queries, and
    // single document retrievals
    match /{path=**}/posts/{post} {
      allow read: if request.auth != null;
    }
    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth != null && request.auth.uid == resource.data.author;

    }
  }
}

Однако обратите внимание, что эти правила будут применяться ко всем коллекциям с posts ID, независимо от иерархии. Например, эти правила применяются ко всем следующим коллекциям posts :

  • /posts/{postid}
  • /forums/{forumid}/posts/{postid}
  • /forums/{forumid}/subforum/{subforumid}/posts/{postid}

Безопасные запросы на групповую коллекцию на основе поля

Как и запросы к отдельным коллекциям, запросы к группам коллекций также должны соответствовать ограничениям, установленным вашими правилами безопасности. Например, мы можем добавить published поле к каждому сообщению на форуме, как мы сделали в примере stories выше:

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
  published: false
}

Затем мы можем написать правила для группы сбора posts на основе статуса published и author сообщения:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    // Returns `true` if the requested post is 'published'
    // or the user authored the post
    function authorOrPublished() {
      return resource.data.published == true || request.auth.uid == resource.data.author;
    }

    match /{path=**}/posts/{post} {

      // Anyone can query published posts
      // Authors can query their unpublished posts
      allow list: if authorOrPublished();

      // Anyone can retrieve a published post
      // Authors can retrieve an unpublished post
      allow get: if authorOrPublished();
    }

    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth.uid == resource.data.author;
    }
  }
}

Используя эти правила, клиенты Web, Apple и Android могут делать следующие запросы:

  • Любой желающий может получить доступ к опубликованным сообщениям на форуме:

    db.collection("forums/technology/posts").where('published', '==', true).get()
    
  • Любой желающий может получить опубликованные сообщения автора на всех форумах:

    db.collectionGroup("posts").where("author", "==", "some_auth_id").where('published', '==', true).get()
    
  • Авторы могут получить доступ ко всем своим опубликованным и неопубликованным сообщениям на всех форумах:

    var user = firebase.auth().currentUser;
    
    db.collectionGroup("posts").where("author", "==", user.uid).get()
    

Защита и запрос документов на основе группы коллекций и пути к документу

В некоторых случаях может потребоваться ограничить запросы к группам коллекций на основе пути к документу. Для создания таких ограничений можно использовать те же методы, что и для защиты и запросов к документам на основе поля.

Рассмотрим приложение, которое отслеживает транзакции каждого пользователя на нескольких фондовых и криптовалютных биржах:

/users/{userid}/exchange/{exchangeid}/transactions/{transaction}

{
  amount: 100,
  exchange: 'some_exchange_name',
  timestamp: April 1, 2019 at 12:00:00 PM UTC-7,
  user: "some_auth_id",
}

Обратите внимание на поле user . Несмотря на то, что мы знаем, какой пользователь владеет документом transaction , исходя из пути к документу, мы дублируем эту информацию в каждом документе transaction , поскольку это позволяет нам делать две вещи:

  • Создавайте запросы на группировку коллекций, ограниченные документами, содержащими определённый /users/{userid} в пути к документу. Например:

    var user = firebase.auth().currentUser;
    // Return current user's last five transactions across all exchanges
    db.collectionGroup("transactions").where("user", "==", user).orderBy('timestamp').limit(5)
    
  • Применить это ограничение для всех запросов к группе сбора transactions , чтобы один пользователь не мог получить документы transaction другого пользователя.

Мы применяем это ограничение в наших правилах безопасности и включаем проверку данных для поля user :

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    match /{path=**}/transactions/{transaction} {
      // Authenticated users can retrieve only their own transactions
      allow read: if resource.data.user == request.auth.uid;
    }

    match /users/{userid}/exchange/{exchangeid}/transactions/{transaction} {
      // Authenticated users can write to their own transactions subcollections
      // Writes must populate the user field with the correct auth id
      allow write: if userid == request.auth.uid && request.data.user == request.auth.uid
    }
  }
}

Следующие шаги