Ограничение доступа для детей и полей с помощью правил безопасности
Я пишу приложение, которое позволяет пользователям отправлять кандидатуры, которые модерируются перед тем, как показывать другим пользователям. Для этого требуется ряд ограничений, которые до сих пор не удались при реализации с правилами безопасности:
- Скрыть любые кандидатуры, которые еще не были одобрены
- Скрыть частные поля из представления (телефон, статус утверждения, дата создания и т.д.).
Мои текущие правила:
{
"rules": {
"nominations": {
".read": true,
"$nominationId": {
".read": "data.child('state').val() == 'approved' || auth != null", // Only read approved nominations if not authenticated
".write": "!data.exists()", // Only allow new nominations to be created
"phone": {
".read": "auth != null" // Only allow authenticated users to read phone number
},
"state": {
".read": "auth != null", // Only allow authenticated users to read approval state
".write": "auth != null" // Only allow authenticated users to change state
}
}
}
}
}
Правила для детей (например, $nomination
) не препятствуют чтению всего ребенка от родителя. Если я слушаю child_added
на https://my.firebaseio.com/nominations, он с радостью возвращает всех детей и все их данные даже с указанными выше правилами безопасности.
Моя текущая обходная идея для этого состоит в том, чтобы сохранить отдельный node с именем approved
и просто перемещать данные между списками всякий раз, когда кто-то одобряет или отклоняет назначение, но он выглядит как ужасно нарушенный подход.
Обновление
Следуя Майкл Леэнбауэр отличный комментарий Я выполнил первоначальную идею с минимальными усилиями.
Новая структура данных выглядит следующим образом:
my-firebase
|
`- nominations
|
`- entries
| |
| `- private
| `- public
|
`- status
|
`- pending
`- approved
`- rejected
Каждая номинация хранится в entries
с частными данными, такими как номер телефона, адрес электронной почты и т.д. под private
и общедоступными данными под public
.
Обновленные правила:
{
"rules": {
"nominations": {
"entries": {
"$id": {
".write": "!data.exists()",
"public": {
".read": true,
},
"private": {
".read": "auth != null"
}
}
},
"status": {
"pending": {
".read": "auth != null",
"$id": {
".write": "root.child('nominations/entries').child($id).exists() && (auth != null || newData.val() == true)"
}
},
"approved": {
".read": true,
"$id": {
".write": "root.child('nominations/entries').child($id).exists() && auth != null"
}
},
"rejected": {
".read": "auth != null",
"$id": {
".write": "root.child('nominations/entries').child($id).exists() && auth != null"
}
}
}
}
}
}
И реализация JavaScript:
var db = new Firebase('https://my.firebaseio.com')
var nominations = db.child('nominations')
var entries = nominations.child('entries')
var status = nominations.child('status')
var pending = status.child('pending')
var approved = status.child('approved')
var rejected = status.child('rejected')
// Create nomination via form input (not shown)
var createNomination = function() {
var data = {
public: {
name: 'Foo',
age: 20
},
private: {
createdAt: new Date().getTime(),
phone: 123456
}
}
var nomination = entries.push()
nomination.setWithPriority(data, data.private.createdAt)
pending.child(nomination.name()).set(true)
}
// Retrieve current nomination status
var getStatus = function(id, callback) {
approved.child(id).once('value', function(snapshot) {
if (snapshot.val()) {
callback(id, 'approved')
} else {
rejected.child(id).once('value', function(snapshot) {
callback(id, snapshot.val() ? 'rejected' : 'pending')
})
}
})
}
// Change status of nomination
var changeStatus = function(id, from, to) {
status.child(from).child(id).remove()
status.child(to).child(id).set(true)
}
Единственной частью реализации, с которой я борюсь, является обработка изменений состояния, мой текущий подход, безусловно, может быть улучшен:
_.each([pending, approved, rejected], function(status) {
status.on('child_added', function(snapshot) {
$('#' + snapshot.name()).removeClass('pending approved rejected').addClass(status.name())
})
})
Я планировал использовать child_changed
на nominations/status
, но я не смог заставить его работать надежно.
Ответы
Ответ 1
Като верно. Важно понимать, что правила безопасности никогда не фильтруют данные. В любом месте вы либо сможете прочитать все данные (включая их дочерние элементы), либо ничего из них. Таким образом, в случае ваших правил наличие ".read": true в "номинациях" отменяет все остальные ваши правила.
Таким образом, подход, который я рекомендовал бы здесь, состоит в том, чтобы иметь 3 списка. Один содержит данные номинации, один содержит список утвержденных номинаций, а другой - список ожидающих номинаций.
Ваши правила могут быть такими:
{
"rules": {
// The actual nominations. Each will be stored with a unique ID.
"nominations": {
"$id": {
".write": "!data.exists()", // anybody can create new nominations, but not overwrite existing ones.
"public_data": {
".read": true // everybody can read the public data.
},
"phone": {
".read": "auth != null", // only authenticated users can read the phone number.
}
}
},
"approved_list": {
".read": true, // everybody can read the approved nominations list.
"$id": {
// Authenticated users can add the id of a nomination to the approved list
// by creating a child with the nomination id as the name and true as the value.
".write": "auth != null && root.child('nominations').child($id).exists() && newData.val() == true"
}
},
"pending_list": {
".read": "auth != null", // Only authenticated users can read the pending list.
"$id": {
// Any user can add a nomination to the pending list, to be moderated by
// an authenticated user (who can then delete it from this list).
".write": "root.child('nominations').child($id).exists() && (newData.val() == true || auth != null)"
}
}
}
}
Неаутентифицированный пользователь может добавить новую номинацию с помощью:
var id = ref.child('nominations').push({ public_data: "whatever", phone: "555-1234" });
ref.child('pending_list').child(id).set(true);
Аутентифицированный пользователь может подтвердить сообщение с:
ref.child('pending_list').child(id).remove();
ref.child('approved_list').child(id).set(true);
И для рендеринга утвержденных и ожидающих списков вы должны использовать код примерно так:
ref.child('approved_list').on('child_added', function(childSnapshot) {
var nominationId = childSnapshot.name();
ref.child('nominations').child(nominationId).child('public_data').on('value', function(nominationDataSnap) {
console.log(nominationDataSnap.val());
});
});
Таким образом, вы используете утвержденные_списки и pending_list в качестве облегченных списков, которые можно перечислять (соответственно неаутентифицированными и прошедшими проверку подлинности пользователями) и хранить все фактические данные номинации в списке номинаций (который никто не может перечислить напрямую).
Ответ 2
Если я полностью пойму, как работают правила безопасности (я просто изучаю их самостоятельно), тогда, когда какое-либо одно правило разрешает доступ, предоставляется доступ. Таким образом, они считываются следующим образом:
- номинации ".read": true, ACCESS GRANTED
- другие правила: не читать
Кроме того, если это правило удалено, $nominationId
".read" предоставляет доступ, если запись одобрена; поэтому .read
в phone
и state
становится излишним всякий раз, когда он одобряется.
Вероятно, было бы проще разбить это на public/
и private/
дети, например:
nominations/unapproved/ # only visible to logged in users
nominations/approved/ # visible to anyone (move record here after approval)
nominations/approved/public/ # things everyone can see
nominations/approved/restricted/ # things like phone number, which are restricted
UPDATE
Подумав об этом, я думаю, что вы все равно столкнетесь с проблемой создания approved/
public, которая позволит вам перечислить записи и иметь approved/restricted/
private. Ограниченным данным может понадобиться также собственный путь в этом случае.
Ответ 3
этот поток немного устарел и может иметь решение по правилам, но, как говорится в видео, его опрятный трюк:
https://youtu.be/5hYMDfDoHpI?t=8m50s
Это может быть не очень хорошая практика, так как db firebase говорят, что правила не являются фильтрами:
https://firebase.google.com/docs/database/security/securing-data
Я не специалист по безопасности, но я проверил трюк, и это сработало хорошо для меня.:)
Поэтому я надеюсь лучше понять проблемы безопасности вокруг этой реализации.