Непредвиденное поведение правила безопасности Firestore метода hasAny() в зависимости от порядка

Я пытаюсь использовать правила безопасности, чтобы ограничить, какую информацию о других пользователях может видеть один аутентифицированный пользователь, в зависимости от того, находятся ли эти пользователи в одном отделе или нет. У меня около 50 пользователей в Firebase Auth, чьи идентификаторы коррелируют с идентификатором документа в коллекции пользователей (usuarios) в Firestore. В коллекции каждый документ имеет параметр с названием Departments, который представляет собой массив строк и представляет отделы, к которым принадлежит пользователь. Тогда моя цель — ограничить доступ на чтение для пользователей, у которых есть какой-либо общий отдел. Для этого я создал это правило безопасности:

match /usuarios/{userId} { 
  allow read: 
    if request.auth != null 
    && "departments" in get(/databases/$(database)/documents/usuarios/$(request.auth.uid)).data.keys() 
    && "departments" in resource.data.keys() 
    && get(/databases/$(database)/documents/usuarios/$(request.auth.uid)).data.departments.hasAny(resource.data.departments); 
}

Я взял двух пользователей (для простоты в объяснении userA и userB, но на примере DLuQJrAy... и DoF7SKfp...) коллекции и добавляю им поле Departments с массивом из одного значения ("department1" в пример) в каждом из них все остальные пользователи документа остаются без поля отделов. Итак, если я прошел аутентификацию с пользователем А и попытаюсь прочитать пользователя Б, мне должно быть разрешено. Если я использую область правил тестирования внутри Firestore, она работает успешно, но если я делаю запрос из веб-клиента Flutter, я получаю сообщение «Отсутствуют или недостаточные разрешения».

Этот код ниже является кодом флаттера в приведенном выше примере. Сначала я запрашиваю того же пользователя, который прошел аутентификацию, чтобы получить информацию о своих отделах, а затем делаю запрос ко всей коллекции с предложениемwhere, чтобы отфильтровать тех, кто имеет какое-либо значение в массиве моих отделов.

DocumentSnapshot usuarioSnapshot = await firestore.collection('usuarios').doc(FirebaseAuth.instance.currentUser?.uid).get();
Map<String, dynamic> usuarioMap = usuarioSnapshot.data() as Map<String, dynamic>;
QuerySnapshot? usuariosSnapshot;
try { 
  usuariosSnapshot = await firestore.collection('usuarios').where("departments", arrayContainsAny: usuarioMap["departments"]).get(); 
} catch (e) {     
  debugPrint("Exception: $e");   
}

На удивление, я хорошо получаю первый звонок и вижу поле отделов со значением ["департамент1"], но затем я попал в загвоздку с «Отсутствуют или недостаточные разрешения».

Что еще более странно, так это то, что если я изменю свое правило безопасности, чтобы напрямую использовать значение ["department1"] вместо resources.data.departments в параметре функции array.hasAny(), то запрос от флаттера-клиента будет работать хорошо и Я получаю только двух пользователей с отделом1 (userA и userB).

match /usuarios/{userId} { 
  allow read: 
    if request.auth != null 
    && "departments" in get(/databases/$(database)/documents/usuarios/$(request.auth.uid)).data.keys() 
    && "departments" in resource.data.keys() 
    && get(/databases/$(database)/documents/usuarios/$(request.auth.uid)).data.departments.hasAny(["department1"]); 
}

Мне кажется, это связано не с флаттер-запросом, а с тем, как ведет себя правило безопасности, если запрос приходит от клиента или выполняется непосредственно из области тестирования. Поскольку простое изменение ресурса.data.departments на ["department1"] (что то же самое, также с точки зрения тестовой области) заставляет его работать.

РЕДАКТИРОВАТЬ (новый тест) Я только что отредактировал правило безопасности, чтобы оно было таким, и оно сработало! Но я не понимаю, почему он так себя ведет: для меня, если arrayA.hasAny(arrayC) && arrayB.hasAny(arrayC) верен, то arrayA.hasAny(arrayB) тоже верен.

match /usuarios/{userId} { 
  allow read: 
    if request.auth != null 
    && "departments" in get(/databases/$(database)/documents/usuarios/$(request.auth.uid)).data.keys() 
    && "departments" in resource.data.keys() 
    && get(/databases/$(database)/documents/usuarios/$(request.auth.uid)).data.departments.hasAny(["department1"])
    && resource.data.departments.hasAny(["department1"]); 
}

Ваш код не очень хорошо отформатирован и его трудно читать. Я предлагаю потратить некоторое время на то, чтобы научиться форматировать код, чтобы другим было легче понять, что вы делаете.

Doug Stevenson 08.07.2024 14:41

Можете ли вы взглянуть на эту ветку . hasAny() метод принимает параметры списка, а resources.data возвращает карту всех полей и значений, хранящихся в документе. Это может быть причиной различного поведения, которое вы испытываете.

Roopa M 08.07.2024 16:55

@RoopaM Это действительно хороший момент: вы предполагаете, что ресурс.data.departments не равен ["department1"], даже если эмулятор показывает, что это так. Я проверил ветку, которой вы только что поделились, и я "соответствую" всему, что в ней говорится, поэтому я продолжаю думать, что проблема в том, что ресурс.data.departments не является ["department1"], даже если это должно быть. Конечно, есть какой-то маленький кусочек, которого я не вижу... но я не знаю, что это может быть.

Aleix Rué 08.07.2024 19:14

@DougStevenson Я к этому не привык, в будущем я проверю ссылку на правильность форматирования. Я только что прочитал это medium.com/firebase-developers/…, которое вы написали. Моя проблема связана с последним случаем (членство в массиве), но вместо проверки одного значения в массиве я ищу любое значение, присутствующее в обоих массивах, с помощью hasAny(). Но arrayA.hasAny(arrayB) получил другой результат, если arrayB жестко запрограммирован ["department1"] или resources.data.departments (значение равно ["department1"], как вы можете видеть на первом изображении)

Aleix Rué 08.07.2024 19:25

Когда вы передаете ["department1"] в hasAny() имя поля, которое равно String, и метод будет искать, есть ли какие-либо поля с таким именем. resource.data.departments содержит карту данных документа. Для большего понимания ознакомьтесь с этим документом.

Roopa M 08.07.2024 20:36

@RoopaM Я понимаю, о чем вы говорите, но я думаю, что вы имеете в виду ресурс.данные, а не ресурс.данные.департменты. Resource.data действительно представляет собой карту, представляющую все поля документа с каждым соответствующим значением, но отделы являются ключом этой карты, поэтому вызов ресурса.data.departments должен вернуть мне значение отделов, которое определено как массив и имеет одно значение, внутри которого находится строка «department1». Кстати, ваш комментарий привел меня к новому тесту, и результат делает его еще более странным, по крайней мере, с моей точки зрения. Пожалуйста, проверьте нижнюю часть сообщения, которое я добавил. Спасибо

Aleix Rué 08.07.2024 21:41

@Aleix Rué, прошу прощения за внесенную мной путаницу. Как упоминалось в этой темыhasAny(), это метод array типов. Но, как вы упомянули, resource.data.departments возвращается String. Я надеюсь, что это прояснит ваши сомнения.

Roopa M 09.07.2024 16:08

@RoopaM Извините за мою путаницу, тогда ресурс.data.departments НЕ возвращает строку, а возвращает список, и этот LIst содержит одну строку внутри. Вот почему я не могу понять, почему не получается... Должно работать

Aleix Rué 09.07.2024 16:28

@Aleix Rué, resources.data.departments возвращает строку То есть «departments1» и hasAny() работают с массивами, а не со строками. Вот почему это терпело неудачу.

Roopa M 10.07.2024 15:10

Опять же, это НЕ строка, resources.data.departments — это массив, но он имеет только одно значение — строку «department1», но само поле представляет собой массив, поэтому hasAny() должен работать. Посмотрите ответы, которые я опубликовал, возможно, вы понимаете причину и обходной путь.

Aleix Rué 11.07.2024 08:44
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
10
104
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Я только что нашел обходной путь, но не понимаю, почему другой способ не работает. Я не уверен, что я что-то упускаю или у нас есть небольшая ошибка.

Дело в том, что если я инвертирую коэффициенты метода .hasAny(), я получу ожидаемое поведение. То есть вместо arrayA.hasAny(arrayB) я сделал arrayB.hasAny(arrayA) и теперь могу запрашивать нужные документы у клиента, не получая ошибки «Отсутствуют или недостаточны разрешения».

Поэтому вместо этого правила безопасности

match /usuarios/{userId} { 
  allow read: 
    if request.auth != null 
    && "departments" in get(/databases/$(database)/documents/usuarios/$(request.auth.uid)).data.keys() 
    && "departments" in resource.data.keys() 
    && get(/databases/$(database)/documents/usuarios/$(request.auth.uid)).data.departments.hasAny(resource.data.departments); 
}

Я написал это

match /usuarios/{userId} { 
  allow read: 
    if request.auth != null 
    && "departments" in get(/databases/$(database)/documents/usuarios/$(request.auth.uid)).data.keys() 
    && "departments" in resource.data.keys() 
    && resource.data.departments.hasAny(get(/databases/$(database)/documents/usuarios/$(request.auth.uid)).data.departments); 
}

И теперь запрос с предложениемwhere может быть успешно выполнен, и правило безопасности работает нормально, потому что если я попытаюсь запросить всю коллекцию без фильтрации, я получу ошибку разрешения.

Мне кажется, что ресурс.data.departments не читается/обрабатывается как список строк, когда он передается в качестве аргумента методу hasAny(), но это происходит в том случае, если это объект, вызывающий метод (и это не удалось только тогда, когда Я запрашивал коллекцию с помощью фильтра, когда запрашивал один документ, все работало хорошо).

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

ПОДДЕРЖКА FIREBASE ОТВЕТ НА МОЙ РЕШЕНИЕ И ПОТЕНЦИАЛЬНАЯ ПРИЧИНА, ПОЧЕМУ ОНО РАБОТАЕТ ТОЛЬКО В ОДНОСТОРОННУЮ СПОСОБ

Поведение, с которым вы сталкиваетесь при использовании правил безопасности Firestore и метода .hasAny(), может быть связано с тем, как правило оценивается во время запроса. Вот разбивка того, что может произойти:

Оценка правил безопасности:

Правила безопасности Firestore оцениваются во время выполнения запроса. Правило получает доступ к данным из нескольких документов, используя вызовы get внутри самого правила.

Потенциальная проблема:

Порядок вычислений внутри вызова .hasAny() может повлиять на результат. Firestore может оценивать аргументы .hasAny() слева направо.

Сценарий 1 (Неработающее правило):

  1. get(/databases/$(database)/documents/usuarios/$(request.auth.uid)) вызывается для получения отделов текущего пользователя. (Давайте назовем это userDepartments)
  2. Доступ к ресурсу.data.departments (отделы запрошенного документа). (Назовем это docDepartments)
  3. userDepartments.hasAny(docDepartments) оценивается.

В этом случае userDepartments могут быть получены не полностью, когда произойдет сравнение. Это может привести к ошибке «Недостаточно разрешений», поскольку правило может не содержать всех данных, необходимых для принятия решения.

Сценарий 2 (Рабочее правило):

  1. Доступ к ресурсу.data.departments (docDepartments).
  2. get(/databases/$(database)/documents/usuarios/$(request.auth.uid)) вызывается для получения отделов текущего пользователя (userDepartments).
  3. docDepartments.hasAny(userDepartments) оценивается.

Здесь в первую очередь осуществляется доступ к docDepartments. Поскольку это часть запрашиваемого документа, он, скорее всего, будет доступен до того, как правило попытается получить доступ к данным пользователя. Затем извлекается userDepartments. Даже если произойдет небольшая задержка, сравнение все равно может работать, поскольку документ docDepartments уже доступен. Хотя поведение, с которым вы столкнулись, является неожиданным с точки зрения оценки правил безопасности Firestore, трудно однозначно сказать, что это ошибка.

Вот почему:

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

Другие вопросы по теме