【问题标题】:Restricting child/field access with security rules使用安全规则限制子/字段访问
【发布时间】:2012-12-27 02:59:55
【问题描述】:

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

  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)不会阻止从父级读取整个子级。如果我在https://my.firebaseio.com/nominations 上收听child_added,即使有上述安全规则,它也会愉快地返回所有孩子及其所有数据。

我目前的解决方法是保留一个名为 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())
    })
})

我计划在nominations/status 上使用child_changed,但我无法让它可靠地工作。

【问题讨论】:

    标签: firebase firebase-realtime-database


    【解决方案1】:

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

    • 提名“.read”:正确,已授予访问权限
    • 其他规则:未读

    此外,如果删除该规则,$nominationId“.read”会在记录被批准的情况下授予访问权限;因此,只要获得批准,phonestate 中的 .read 就会变得多余。

    将其分解为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/ 设为私有。在这个用例中,受限数据可能也需要自己的路径。

    【讨论】:

      【解决方案2】:

      加藤是对的。重要的是要了解安全规则从不过滤数据。对于任何位置,您要么能够读取所有数据(包括其子数据),要么无法读取。因此,就您的规则而言,在“提名”下具有 ".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 用作可以枚举的轻量级列表(分别由未经身份验证和经过身份验证的用户),并将所有实际提名数据存储在提名列表中(没有人可以直接枚举)。

      【讨论】:

      • 在我看来,任何需要过滤或隐藏最终用户信息的应用程序都会遇到类似的问题,坦率地说,您概述的方法只是证明我可能应该重新考虑使用 Firebase具有此类要求的项目。
      • 很公平。虽然我会记住几件事。首先,像这样对数据进行非规范化起初可能会让人感到尴尬,但这是一种完全有效的方法。例如,当您查看 Twitter 提要时,Twitter 不会进行任何“过滤”。这一切都是提前预先计算好的。这会使用额外的空间,并可能导致临时不一致的数据,但它的扩展性比加入/过滤数据要好得多。其次,您要寻找的基本上是每行访问控制,我认为这在任何数据库中都很难找到。如果我能以任何方式提供帮助,请随时联系(firebase dot com 的迈克尔)!
      • 举一个@SimenBrekken 观点的例子:聊天室希望允许用户查看在joinDateleaveDate 之间发送的消息。我们可以在查询中进行过滤以用于显示目的,但最终用户仍然可以访问他们不应该看到的消息。由于所有日期都是动态的,我们不能闯入approved_listpending_list 等。@michael-lehenbauer,是否可以使用 Firebase 功能来保护数据访问控制?我在 FB 文档中没有看到 onRead()
      • @MichaelLehenbauer 其次,您要寻找的基本上是每行访问控制,我认为这在任何数据库中都很难找到。具有讽刺意味的是,这通常很难(或不完整)在许多数据库中,因为用户登录并不总是 1:1 映射到数据库登录。有了firebase,这个问题就解决了。简单的一点应该是“仅显示./user_id 字段与登录用户匹配的记录”,或“仅显示登录用户组有权访问该记录的记录”。
      【解决方案3】:

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

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

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

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

      【讨论】:

      • 最好把答案放在这里,而不是可能死掉的链接...
      • 上面的视频没有违反“规则不是过滤器”的声明——也就是说,它不是一个试图绕过它的技巧。这只是一种结构化数据的方式,可以有效地与规则系统配合使用。
      猜你喜欢
      相关资源
      最近更新 更多