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

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

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

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

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

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

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

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

/истории/{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;

    }
  }
}

Обратите внимание, однако, что эти правила будут применяться ко всем коллекциям с ID posts , независимо от иерархии. Например, эти правила применяются ко всем следующим коллекциям 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
    }
  }
}

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