Ограничение доступа детей / полей с помощью правил безопасности

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

  1. Скрыть все кандидатуры, которые еще не были утверждены
  2. Скрыть личные поля от отправки (телефон, статус утверждения, дата создания и т. Д.)

Мои текущие правила таковы:

{
    "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 он с радостью возвращает всех детей и все их данные даже с соблюдением вышеуказанных правил безопасности.

Моя текущая идея обхода этого заключается в том, чтобы сохранить отдельный узел с именем 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 но я не смог заставить его работать надежно.

3 ответа

Решение

Като прав. Важно понимать, что правила безопасности никогда не фильтруют данные. В любом месте вы либо сможете прочитать все данные (включая их дочерние элементы), либо ничего из них. Так что в случае ваших правил наличие ".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 в качестве облегченных списков, которые могут быть перечислены (неаутентифицированными и аутентифицированными пользователями соответственно) и сохраните все фактические данные номинации в списке номинаций (который никто не может перечислить напрямую).

Если я полностью уклоняюсь от того, как работают правила безопасности (я просто изучаю их самостоятельно), то когда любое одно правило разрешает доступ, доступ предоставляется. Таким образом, они читаются следующим образом:

  • номинации ".read": правда, ДОСТУП ПРЕДОСТАВЛЕН
  • другие правила: не читать

Кроме того, если это правило будет удалено, $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

ОБНОВИТЬ

Думая об этом еще больше, я думаю, что вы все равно столкнетесь с проблемой approved/ общедоступный, что позволит вам перечислить approved/restricted/ частный. Ограниченные данные могут также нуждаться в собственном пути в этом случае использования.

Этот поток немного устарел и может иметь решение с помощью правил, но, как говорится в видео, это хитрый трюк: https://youtu.be/5hYMDfDoHpI?t=8m50s

Это может быть не очень хорошей практикой, поскольку в документации Firebase говорится, что правила не являются фильтрами: https://firebase.google.com/docs/database/security/securing-data

Я не специалист по безопасности, но я проверил трюк, и он работал хорошо для меня.:)

Поэтому я надеюсь, что лучшее понимание вопросов безопасности вокруг этой реализации.

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