It was very hard for me to understand to start with, but the rules are very flexible and let you base access to your data based on other database contents. Basically you grant access to a node and that grant applies to all children as well and can't be removed from deeper nodes in the tree. You can apply those database rules in the console, but there must be an api if you need to use it as well to set the rules for the entire database. They do have to be a single document, so you wouldn't want to hard-code users, but you could put them in a hidden and inaccessible node, the rules have access to it.
For instance let's say you want to let people request to be friends with other people, and for the other person to be able to accept and add both people to the friends list. You could have a schema similar to this:
uid
"friends"
friendUid1
friendUid2
friendUid3
"private"
... some private data your friends can read
"friendRequests"
targetUid
requestorUid -> can be written to only by requestorUid
The first step is writing a value to friendRequests/$targetUid/$requestorUid
. The ONLY person that can write to this node is the one authenticated as requestorUid. targetUid would be granted read access to targetUid node so they could read it because it is a child, but not write it.
Then you could grant write access to $requestor/friends/$targetUid
to $targetUid based on the existence of friendRequests/targetUid/requestorUid
. This allows the person that received the friend request to write their OWN uid to the requestor's friend list, but only if the requestor had already write their own uid in a request to be a friend. They would then write the requestor's uid to their own friends list. If the uid of a logged-in user is in their friends list, they can access the private data.
{
"rules": {
"$uid": {
".read": "auth.uid === $uid",
".write": "auth.uid === $uid",
"friends": {
"$friendId": {
".write": "root.child('friendRequests').child($friendId).child($uid) && auth.uid === $friendId"
}
},
"private": {
".read": "data.parent().child('friends').child(auth.uid).exists()"
}
},
"friendRequests": {
"$targetUid": {
".read": "auth.uid === $targetUid",
"$requestorUid": {
".write": "auth.uid === $requestorUid"
}
}
}
}
}
Let's use some "real" ids and say that uid 100 wants to be friends with uid 200. They would write their own id 100 to be a request for 200, this is allowed because auth.uid will match $requestorUid and match the last write rule:
ref = db.getReference("friendRequests/200/100");
ref.setValue(true);
When user id 200 logs in, they can read all their friend requests at friendRequests/200
. They see that user 100 requested to be their friend, so they first add 100 to users/200/friends
. This is allowed because auth.uid will be 200 and they have full read/write access to the entire users/200
node and all its children.
Next they can also write to users/100/friends/200
because this rule:
"root.child('friendRequests').child($friendId).child($uid) && auth.uid === $friendId"
auth.uid
will be 200 and the check will see that 100 asked to become friends with 200 because friendRequests/200/100
exists and that node is only writable by user 100.