Я пытаюсь использовать правила безопасности, чтобы ограничить, какую информацию о других пользователях может видеть один аутентифицированный пользователь, в зависимости от того, находятся ли эти пользователи в одном отделе или нет. У меня около 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"]);
}
Можете ли вы взглянуть на эту ветку . hasAny() метод принимает параметры списка, а resources.data возвращает карту всех полей и значений, хранящихся в документе. Это может быть причиной различного поведения, которое вы испытываете.
@RoopaM Это действительно хороший момент: вы предполагаете, что ресурс.data.departments не равен ["department1"], даже если эмулятор показывает, что это так. Я проверил ветку, которой вы только что поделились, и я "соответствую" всему, что в ней говорится, поэтому я продолжаю думать, что проблема в том, что ресурс.data.departments не является ["department1"], даже если это должно быть. Конечно, есть какой-то маленький кусочек, которого я не вижу... но я не знаю, что это может быть.
@DougStevenson Я к этому не привык, в будущем я проверю ссылку на правильность форматирования. Я только что прочитал это medium.com/firebase-developers/…, которое вы написали. Моя проблема связана с последним случаем (членство в массиве), но вместо проверки одного значения в массиве я ищу любое значение, присутствующее в обоих массивах, с помощью hasAny(). Но arrayA.hasAny(arrayB) получил другой результат, если arrayB жестко запрограммирован ["department1"] или resources.data.departments (значение равно ["department1"], как вы можете видеть на первом изображении)
Когда вы передаете ["department1"] в hasAny()
имя поля, которое равно String
, и метод будет искать, есть ли какие-либо поля с таким именем. resource.data.departments
содержит карту данных документа. Для большего понимания ознакомьтесь с этим документом.
@RoopaM Я понимаю, о чем вы говорите, но я думаю, что вы имеете в виду ресурс.данные, а не ресурс.данные.департменты. Resource.data действительно представляет собой карту, представляющую все поля документа с каждым соответствующим значением, но отделы являются ключом этой карты, поэтому вызов ресурса.data.departments должен вернуть мне значение отделов, которое определено как массив и имеет одно значение, внутри которого находится строка «department1». Кстати, ваш комментарий привел меня к новому тесту, и результат делает его еще более странным, по крайней мере, с моей точки зрения. Пожалуйста, проверьте нижнюю часть сообщения, которое я добавил. Спасибо
@Aleix Rué, прошу прощения за внесенную мной путаницу. Как упоминалось в этой темыhasAny()
, это метод array
типов. Но, как вы упомянули, resource.data.departments
возвращается String
. Я надеюсь, что это прояснит ваши сомнения.
@RoopaM Извините за мою путаницу, тогда ресурс.data.departments НЕ возвращает строку, а возвращает список, и этот LIst содержит одну строку внутри. Вот почему я не могу понять, почему не получается... Должно работать
@Aleix Rué, resources.data.departments возвращает строку То есть «departments1» и hasAny() работают с массивами, а не со строками. Вот почему это терпело неудачу.
Опять же, это НЕ строка, resources.data.departments — это массив, но он имеет только одно значение — строку «department1», но само поле представляет собой массив, поэтому hasAny() должен работать. Посмотрите ответы, которые я опубликовал, возможно, вы понимаете причину и обходной путь.
Я только что нашел обходной путь, но не понимаю, почему другой способ не работает. Я не уверен, что я что-то упускаю или у нас есть небольшая ошибка.
Дело в том, что если я инвертирую коэффициенты метода .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 (Неработающее правило):
В этом случае userDepartments могут быть получены не полностью, когда произойдет сравнение. Это может привести к ошибке «Недостаточно разрешений», поскольку правило может не содержать всех данных, необходимых для принятия решения.
Сценарий 2 (Рабочее правило):
Здесь в первую очередь осуществляется доступ к docDepartments. Поскольку это часть запрашиваемого документа, он, скорее всего, будет доступен до того, как правило попытается получить доступ к данным пользователя. Затем извлекается userDepartments. Даже если произойдет небольшая задержка, сравнение все равно может работать, поскольку документ docDepartments уже доступен. Хотя поведение, с которым вы столкнулись, является неожиданным с точки зрения оценки правил безопасности Firestore, трудно однозначно сказать, что это ошибка.
Вот почему:
Ваш код не очень хорошо отформатирован и его трудно читать. Я предлагаю потратить некоторое время на то, чтобы научиться форматировать код, чтобы другим было легче понять, что вы делаете.