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

Эта страница развивает концепции, изложенные в разделах «Структурирование правил безопасности» и «Написание условий для правил безопасности» , и объясняет, как 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 Authentication для установки поля author в UID пользователя, создавшего документ. Firebase Authentication также заполняет переменную 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 , независимо от иерархии. Например, эти правила применяются ко всем следующим коллекциям 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
    }
  }
}

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