I'm creating an application which lets users create items and then allow other users to subscribe to those items. I'm struggling to craft a rule that will prevent users from subscribing more than once to an item.
Here is an example of my data structure (anonymized, hence the "OMITTED" values):
{
"OMITTED" : {
"name" : "Second",
"body" : "this is another",
"userName" : "Some User",
"userId" : "OMITTED",
"created" : 1385602708464,
"subscribers" : {
"OMITTED" : {
"userName" : "Some User",
"userId" : "OMITTED"
}
}
}
}
Here are my Firebase rules at present:
{
"rules": {
".read": true,
".write": "auth != null",
"items": {
"$item": {
".write": "!data.exists()",
".validate": "newData.hasChildren(['name', 'body', 'userId', 'userName']) && newData.child('userId').val() == auth.id",
"subscribers": {
"$sub": {
".validate": "newData.hasChildren(['userId', 'userName']) && newData.child('userId').val() != data.child('userId').val()"
}
}
}
}
}
}
How can I prevent users from subscribing more than once? What is the rule I need to prevent duplicate users within the subscribers list based on userId?
Since security rules can't iterate a list of records to find the one containing a certain bit of data, the trick here is to store the records by an ID which allows for easy access. There is a great article on denormalization which offers some good insights into this practice.
In this case, if your use case allows, you may simply want to switch your data structure so that records are stored by the user's id, rather than storing the ID as a value in the record, like so:
/users/user_id/items/item_id/subscribers/user_id/
In fact, as you'll see in denormalization, you may even benefit from splitting things out even farther, depending on the exact size of your data and how you'll be reading it later:
/users/user_id
/items/user_id/item_id
/subscribers/item_id/user_id
In either of these formats, you can now prevent duplicates and lock down security rather nicely with something like this:
{
"users": {
"$user_id": { ".write": "auth.id === $user_id" }
},
"subscribers": {
"$subscriber_id": { ".write": "auth.id === $subscriber_id" }
}
}
Related
,"people" : {
".read": "auth != null",
"$uid": {
},
"e2" : {
".read": "auth.uid != null",
".write": "$uid == auth.uid"
},
"l1" : {
".read": "auth.uid != null",
".write": "auth.uid != null"
}
So e2 is the # of an email. For example #gmail or #aol or #yahoo. For the l1 child, I want to make the write rule: Write if auth.uid != null && e2 has the same value as the e2 of the person you are writing l1 to. The furthest I could get, is something like this:
"data.parent().child(people).hasChildren(['auth.uid', 'l1'])"
JSON
"people": {
"02PdiNpmW3MMyJt3qPuRyTpHLaw2": {
"e2": "aol.com",
"l1": 4,
"X": {
"e2": "aol.com",
"l1": 0,
"P": {
"e2": "gmail.com",
"l1": 0,
Basically l1 = like, so a user writes to another users's l1 in the form of adding 1 more to the count.
Operation that should succeed:
A user X wants to like the user with uid 02PdiNpmW3MMyJt3qPuRyTpHLaw2. User X has a e2 child of #aol.com. This is the same as the e2 child of the user he wants to like. User x is also an authorized user, therefore he meets the 2 requirements to write to the l1 of the user he wants to like.
Operation that should not succeed:
User P wants to like the user with uid 02PdiNpmW3MMyJt3qPuRyTpHLaw2. User P has a e2 child of #gmail.com. This is not the same as the e2 child of the user he wants to like. User P therefore does not meet the requirement to write to the l1 of the user he wants to like
This is a really involved scenario, so I'm going to walk through it step-by-step. There is a good chance you'll need to make changes to make these rules work for your complete use-case, so I'm hoping that having each step will make it possible for you to tune them yourself.
The first step is to deobfuscate your data structure. I'll instead use this structure to get started:
{
"people" : {
"user1" : {
"domain" : "aol.com",
"likeCount" : 2,
"likers" : {
"user2" : {
"comain" : "aol.com"
},
"user3" : {
"domain" : "aol.com"
}
}
}
}
}
So user1 has two likes, by user2 and user3 both from the same domain.
I highly recommend using meaningful names like this in your database in general, but definitely in questions that you post about it. If people can't easily understand your data model, chances of them helping go down rapidly.
In the above data model, we can ensure that only users from the same domain can like this user with:
"people": {
"$uid": {
"likers": {
"$likerid": {
".write": "data.parent().parent().child('domain').val() == newData.child('domain').val()"
}
}
}
}
With these rules, I've tried two write operations to people/user1/likers/user4. The first operation succeeds:
{
"domain": "aol.com"
}
The second operation fails:
{
"domain": "gmail.com"
}
We should probably also ensure that a user can only write their own likes, and not for other users. We can do that with:
"people": {
"$uid": {
"likers": {
"$likerid": {
".write": "$likerid == auth.uid &&
data.parent().parent().child('domain').val() == newData.child('domain').val()"
}
}
}
}
Next up, we'll add a rule that allows a user to like someone, only if they haven't liked them before. We'll do that on people/$uid as we'll need to look at data under likers and under likesCount soon.
The rules for the first step are:
"people": {
"$uid": {
".write": "
!data.child('likers').child(auth.uid).exists() && newData.child('likers').child(auth.uid).exists()
",
"likers": {
"$likerid": {
".write": "$likerid == auth.uid &&
data.parent().parent().child('domain').val() == newData.child('domain').val()"
}
}
}
So these rules allow us to write to a user, if we are adding a like that doesn't exist yet. You may need to do additional checks here to allow updates of other child nodes, but for here we'll keep things as simple as possible (as it's already pretty involved).
Finally you want to ensure that the write must also increment the likeCount, which should be something like this:
"people": {
"$uid": {
".write": "
!data.child('likers').child(auth.uid).exists() && newData.child('likers').child(auth.uid).exists()
&& newData.child('likeCount').val() == data.child('likeCount').val() + 1
",
"likers": {
"$likerid": {
".write": "$likerid == auth.uid &&
data.parent().parent().child('domain').val() == newData.child('domain').val()"
}
}
}
So the new line now checks if thew new data at likeCount is one higher than its previous value.
I've taken each of the steps above in a test database of my own, and tested in the playground with both positive and negative cases. So while there may be some issues, the basic approach of each step works.
As said this is pretty involved, and it's quite likely that you'll need to make significant changes before it fully works for all your use-cases.
What should be the firebase rules for comment on post which is similar to facebook.
There are two things:
first, only authenticated user can comment.
Second, only the user who has commented can delete the comment. The user who has commented his id is saved in username.
I strongly suggest using Firebase Bolt for writing/compiling Firebase Database Security rules. Data structure can get big and complicated. Using Bolt language you'll be able to easily write complex access and structure rules that can be re-used for other db patterns.
Your rules would look something like this:
path /comment/{postUid}/{commentUid} is Comment {
read() { true }
write() { isAuthor(this) || isAuthor(prior(this)) }
}
type Comment {
text : String,
username : String
}
isAuthor(value) { auth != null && value.username == auth.uid }
Pay attention to isAuthor(prior(this)) call. This is the way to make sure only author can delete a comment. prior function returns data as it was saved before current event (create, update or delete).
After using firebase-bolt tool to compile rules to JSON format you'll get:
{
"rules": {
"comment": {
"$postUid": {
"$commentUid": {
".validate": "newData.hasChildren(['text', 'username'])",
"text": {
".validate": "newData.isString()"
},
"username": {
".validate": "newData.isString()"
},
"$other": {
".validate": "false"
},
".read": "true",
".write": "auth != null && newData.child('username').val() == auth.uid || auth != null && data.child('username').val() == auth.uid"
}
}
}
}
}
How can I add new rule to Firebase Realtime Database so that users can read data where recid equal to user ID?
I have a massage table with this structure:
"messages" : {
"-KyyjeMOtc7fWAsOiuiP" : {
"recid" : "FL5hyQJrsHWRQsRtiLe1PxkyRnk1",
"senderid" : "6K6pQHaCishDlCb0Y9AaN3zI22n1",
"text" : "hi"
},
"-KyykczCNpsSL6a1t8vt" : {
"recid" : "FL5hyQJrsHWRQsRtiLe1PxkyRnk1",
"senderid" : "6K6pQHaCishDlCb0Y9AaN3zI22n1",
"text" : "test"
},
}
I want a rule that when data is added to the database, only the user whose uid is equal to recid can see the data.
To achieve this, you can create user-based security rules for your database, something similar to:
{
"rules": {
"messages": {
"$messageId": {
".read": "auth.uid == data.child('recid').val()",
".write": "auth !== null"
}
}
}
}
In this example, $messageId uses a $location variable that will match any key under your messages list. Then, we grant read access only if the current user's auth.uid matches the recid child value.
I have the following database schema:
{
"events": {
"$eventId": {
"eventTitle": "Go shopping",
"participants": {
"0": {
"id": "0",
"name": "John Smith"
},
"1": {
"id": "1",
"name": "Jason Black"
}
}
}
}
}
It's an array of events, where each event has a list of participants. How to make a database rule, where:
everyone can get event or list of events,
when getting an event, a full list of participants can only by visible by admin,
when getting an event, if a user is a participant of the event, the list of participants would retrieve only him, noone else,
when getting an event, if a user is not a participant, the participant list would be empty
Here is my try in rule scheme:
{
"rules": {
"events": {
".read": true,
"$eventKey": {
"eventTitle": {
".validate": "newData.isString() && newData.val().length < 100"
},
"participants": {
".read": "root.child('users/'+auth.uid+'/role').val() === 'ADMIN'",
".validate": "newData.hasChildren()",
"$participantKey": {
".read": "($participantKey === auth.uid || root.child('users/'+auth.uid+'/role').val() === 'ADMIN')",
"id": {
".validate": "newData.val() === $participantKey"
},
"name": {
".validate": "newData.isString() && newData.val().length < 100"
}
}
}
}
}
}
}
It does not work, because when I read events list it doesn't respect .read constraint in participants and $participantKey fields. It just retrieves full list of participants all the time.
#edit
In other words. I have this simplified rules:
{
"events": {
".read": true,
"$eventKey": {
"participants": {
".read": false
}
}
}
}
When I query for: events/{eventKey}/participants I still get an object with participants even though the participants read flag is set to false.
But, when I remove .read flag from events, then retrieving data respects .read flag in participants.
#edit2
From documentation:
A .read rule which grants permission to read a location will also allow reading of any descendants of that location, even if the descendants have their own .read rules which fail.
My question is now, how to omit this rule?
Firebase permissions cascade downwards. Once you've given a user a permission on a certain level in the JSON tree, you cannot revoke that permission on a lower level in the tree.
That means that these rules will not work:
{
"events": {
".read": true,
"$eventKey": {
"participants": {
".read": false
}
}
}
}
The ".read": false is ignored by Firebase.
Instead you will have to structure your data in a way that allows your security requirements. This is done by completely separating the types of data that have different security requirements.
{
"events": {
".read": true,
"$eventKey": {
"participants": {
".read": false
}
}
}
"eventparticipants": {
".read": false
"$eventKey": {
/* This is where you store the participants */
}
}
}
So you have two top-level lists: events and eventparticipants. The lists use the same keys for the objects under them: the event id. But since these are two top-level lists, one can be publicly readable while the other is more restricted.
Firebase documentation recommends against using arrays when adding data to the database. The main problem in your code is that you use an array, which is an anti-pattern when it comes to Firebase.
One of the many reasons Firebase recommends against using arrays is that it makes the security rules impossible to write and this is your case.
Because Firebase is a NoSQL database and becase it is structured as pairs of key and valeu, the solution is to use a Map and not an array. Change the way in which you add data in your database and your problem will be solved.
We are building a platform using Firebase Realtime Database and I'm having a bit of a struggle to find the best way to structure our data for private and public access.
Today we have
database: {
items: {
$userUid: {
$itemUid: {
poster_link: "..."
format: "..."
title: "..."
}
}
}
}
All our items are stored under each user in order to make it fast and secure to load.
Our rules are set up like this
{
"rules": {
"items": {
"$userId": {
"$itemId": {
".read": "auth !== null,
".write": "auth !== null"
}
}
}
}
}
So only an authorised user can read and write the data. I could create something like this to allow items to be public if the value is true:
".read": "auth !== null || data.child('public').val() == true"
But this will still be under $userUid
So I was wondering if you have any suggestion on how to structure this example to allow items to be under a user and also seen publicly, not necessary under this user, a bit like Dropbox does when you share something.
You chosen data structure does not take advantage of the flat data principles of Firebase. This will make it very difficult for you to query items of multiple users. For example, how do you get all the public items without drilling into each user?
Similarly, a boolean called public is also not good because you can't extend it to other ACL scenarios. Much better is an ACL object that can be extended in the future.
For example:
items: {
itemsUid: {
[...],
userId: ...,
ACL: { public: true }
}
}
Now you can write the rule:
auth !== null && (root.child(items/ACL/public).exsists() || data.userId === auth.UID)
If in three months you add a concept of friends that can see you posts or followers that can see you items you can simply add friends: true, followers: true to the ACL object and adjust the rule.
You can structure like this
database: {
items: {
$itemUid: {
poster_link: "..."
format: "..."
title: "..."
user: "userid"
}
}
}
now set the rules as
{
"rules": {
"items": {
"$itemId": {
".read": "auth !== null || data.child('public').val() == true,
".write": "auth !== null"
}
}
}
}