56

我正在编写一个应用程序,允许用户提交提名,这些提名在显示给其他用户之前会经过审核。这需要一些限制,到目前为止,我在使用安全规则方面未能成功实施:

  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,并在有人批准或拒绝提名时简单地在列表之间移动数据,但这似乎是一种非常糟糕的方法。

更新

根据Michael Lehenbauer的出色评论,我以最小的努力重新实现了最初的想法。

新的数据结构如下:

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_changedonnominations/status但我无法让它可靠地工作。

4

3 回答 3

52

加藤是对的。重要的是要了解安全规则从不过滤数据。对于任何位置,您要么能够读取所有数据(包括其子数据),要么无法读取。因此,就您的规则而言,在“提名”下具有“.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());
  });
});

通过这种方式,您可以将approved_list 和pending_list 用作可以枚举的轻量级列表(分别由未经身份验证和经过身份验证的用户),并将所有实际提名数据存储在提名列表中(没有人可以直接枚举)。

于 2013-01-12T22:19:15.820 回答
5

如果我完全理解安全规则的工作方式(我只是自己学习它们),那么当任何一个规则允许访问时,就会授予访问权限。因此,它们被解读如下:

  • 提名“.read”:真实,访问权限
  • 其他规则:未读

此外,如果删除该规则,$nominationId如果记录被批准,“.read”将授予访问权限;因此,只要它被批准, .readinphone和就变得多余了。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/私有。在此用例中,受限数据可能也需要自己的路径。

于 2013-01-12T19:25:34.507 回答
-1

这个线程有点过时了,它可能通过规则有一个解决方案,但正如视频所说,它是一个巧妙的技巧: https ://youtu.be/5hYMDfDoHpI?t=8m50s

这可能不是一个好习惯,因为 firebase 文档说规则不是过滤器: https ://firebase.google.com/docs/database/security/securing-data

我不是安全专家,但我测试了这个技巧,它对我来说效果很好。:)

所以我希望能更好地理解围绕这个实现的安全问题。

于 2016-08-03T21:49:12.317 回答