web-dev-qa-db-ja.com

セキュリティルールを使用して子/フィールドアクセスを制限する

ユーザーが他のユーザーに表示される前にモデレートされたノミネートを送信できるようにするアプリを作成しています。これには、これまでセキュリティルールを実装できなかった多くの制限が必要です。

  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 on 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_changed オン nominations/statusしかし、私はそれを確実に動作させることができませんでした。

49
Simen Brekken

加藤さん。セキュリティルールは決してfilter dataではないことを理解することが重要です。どの場所でも、すべてのデータ(その子を含む)を読み取ることも、まったく読み取ることもできません。したがって、ルールの場合、「指名」の下に「.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());
  });
});

この方法では、承認済みリストと保留中リストを軽量リストとして使用して(それぞれ非認証ユーザーと認証ユーザーによって)列挙し、実際のすべての指名データを指名リスト(誰も直接列挙できない)に保存します。

46

セキュリティルールの仕組みを完全に理解している場合(自分で学習しているだけです)、いずれかのルールがアクセスを許可すると、アクセスが許可されます。したがって、それらは次のように読み取られます。

  • ノミネート ".read":true、アクセス許可
  • その他のルール:未読

さらに、そのルールが削除された場合、$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

[〜#〜] update [〜#〜]

これをさらに考えてみると、approved/をパブリックにすることで、レコードをリストできるようになり、approved/restricted/をプライベートにするという問題がまだ発生すると思います。このユースケースでは、制限されたデータにも独自のパスが必要になる場合があります。

4
Kato

このスレッドは少し時代遅れであり、ルールを介した解決策があるかもしれませんが、ビデオが言っているように、その巧妙なトリック: https://youtu.be/5hYMDfDoHpI?t=8m50s

Firebaseのドキュメントでは、ルールはフィルターではないと言われているため、これは良い習慣ではないかもしれません: https://firebase.google.com/docs/database/security/securing-data

私はセキュリティの専門家ではありませんが、このトリックをテストしたところ、うまく機能しました。 :)

したがって、この実装に関するセキュリティの問題をよりよく理解することを願っています。

0
Rovel