I am struggling to design a simple data sharing app using firebase. To illustrate, consider a messaging app similar to facebook:
Users can add friends - the request must be accepted
Users can add posts
Users can view all posts submitted by themselves and friends
I can set up the security rules for this, but filtering or denormalizing data seems overly cumbersome and inefficient. A sample data model might look like:
{
"users": {
"user1": {
"friends": {
"user2": { "accepted": true }
}
},
"user2": {
"friends": {
"user1": { "accepted": true }
}
},
"user3": {
"friends": {}
}
},
"posts": {
"generatedId1": {
"message": "blah blah",
"user_id": "user1"
},
"generatedId2": {
"message": "lorem ipsum",
"user_id": "user2"
},
"generatedId3": {
"message": "hello world",
"user_id": "user3"
}
}
}
Security rules to achieve the above requirements would look something like:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
"posts": {
"$post": {
".read": "root.child('users').child(data.child('user_id').val()).child('friends').child(auth.uid).child('accepted').val() === true",
".write": "newData.child('user_id').val() === auth.uid"
}
}
}
}
It's probably not quite right, but close enough to illustrate. We can see users can only read and write their own user record, can only add posts with their own user id and can read posts where there is a corresponding accepted friend record.
So my question essentially boils down to how to execute a query that will only return the posts a user is allowed to see? Since security rules are not filters, they do not help us in this case.
My understanding so far is that the posts should be denormalized, basically duplicate the post for each user. This is horribly inefficient - if the user has 100 friends, this means duplicating the data 100 times. Retrospective updates (i.e. adding a new friend or removing an existing one) are also complex to manage.
Another option is to retrieve a users own list of friends and retrieve the posts for those users in separate queries, but again, if a user has 100 friends, this would mean executing 100 separate "queries".
Essentially, I'm attempting to implement a many to many relationship. What is the guidance / best practice here?
Related
So I've set up a simple db/web form to collect some user data. Rn I am trying to figure out the rules thing but I am running into this problem - if my read flag is set to true then I can simply run this in the console
var ref = firebase.database().ref();
ref.on("value", function(snapshot) {
console.log(snapshot.val());
}, function (error) {
console.log("Error: " + error.code);
});
and expose the users which should not be the possibility. If I set read to false then I cant access the DB upfront to validate if email address is unique or not. I guess I need to achieve 2 things:
Prevent db snooping through the dev tools running any snippets
Make sure email address is unique.
p.s. My currents rules (prevent delete, prevent read, make sure POST request has certain fields):
{
"rules": {
"users": {
".read": false,
"$uid": {
".write": "!data.exists()",
".validate": "newData.hasChildren(['first_name', 'last_name', 'email', 'country', 'amount'])"
}
}
}
}
To avoid duplicates you will want a validation check in the DB rather than reading data from the client and checking (you can't trust the client).
Since there is no easy way to check for duplicate child values in a firebase base collection, you will need a separate collection to track emails and then validate your emails against that, i.e.:
{
"rules": {
"users": {
".read": false,
"$uid": {
".write": "!data.exists()",
".validate": "newData.hasChildren(['first_name', 'last_name', 'email', 'country', 'amount'])",
"email": {
".validate": "!root.child('emails').child(newData.val()).exists()"
}
}
},
"emails": {
".read": false,
".write": "!data.exists()"
}
}
}
You will then need to write the users' emails to the email collection as the users are added, e.g.:
var ref = firebase.database().ref('emails/'+email);
ref.set(uid)
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" }
}
}
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.
I am currently trying to implement rate limting by checking the timestamp of the last post and then add + 60sec to it and then check if it is smaller(<) then the current Firebase Server Timestamp(now).
It somehow always returns true and grants access ?!
These are my Rules:
{
"rules": {
"posts": {
".read": true,
".write":
"(root.child('users').child(auth.uid).child('lastPost').val() + 60) < now"
}
}
}
This is my database structure
{
"posts": {
"-KV70ppGGTEtXY4_Q4UC": {
"author": "abcdedef-uid-ojifgoöifjgssgd",
"description": "Thats the post description",
"title": "Thats the post title"
}
},
"users": {
"2uy7323nTodMHcVxeEDJzoexH302": {
"canPost": true,
"email": "cryptic.pug#firebase.com",
"lastPost": 14776667681,
"profile_picture": "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg",
"username": "Cryptic Pug"
}
}
}
Thanks for your Clue Vladimir!
As I haven't found this kind of soulution anyware I would like to share the answer here officially:
{
"rules": {
"posts": {
".read": true,
".write":
"(root.child('users').child(auth.uid).child('lastPost').val() + 60000) < now"
}
}
}
Explanation:
When a user posts something you always update the Value in the Database with the Value of firebase.database.ServerValue.TIMESTAMP to the user information.
In the Rule language you read the Timestamp of the Last Post is read out of the user who wants to post (auth.uid in FB Rule Language) and add 60 seconds (*1000 as Firebase uses Milliseconds in it's timestamp), which would be the time when the user would be allowed to post again. And Then check if the current server timestamp is higher (<) than the time the user is allowed to post again.
Hope It helped you guys, Happy Coding - Doing Firebase for 3 days and it's great!
I keep getting firebase messages on console regarding to add indexOn for user 4321 at chat rules. Below is my database.
And my rules in firebase is like:
{
"rules": {
".read": "auth != null",
".write": "auth != null",
"users": {
".indexOn": ["name", "email","uid"],
"$uid": {
".write": "$uid === auth.uid"
}
},
"chats": {
"$key": {
".indexOn": ["uid"]
}
}
}
}
I'm not sure why the indexing not working. And how can I improve indexing for overall database?
Your current data structure only allows to easily list all members of a chatroom, not the other way around. That may be the reason you get that message, because if you want to list all chats that user belongs to, you have to search through all /chats records.
You probably need to duplicate the chat room membership data both at /chat/<chat-id>/members and /users/<uid>/groups. Your case is almost identical to the one in the Firebase guide here -- read particularly the description below code in the section linked, but it's best to read the whole guide, it really helped me to understand how the database works.
Btw: your rule in chats: ".indexOn": ["uid"] doesn't do anything with the sample data you posted. It says to "index chat rooms by their uid attribute", but your chat rooms don't have an uid key inside (meaning uid: 'someid', not 'someid': true). See the indexing guide on more info how indexing works.
There are two types of indexing orderByValue: and orderByChild
Indexing with orderByValue::
{
"rules": {
"scores": {
".indexOn": ".value"
}
}
}
JSON Tree
{
"employee": {
<"employee1Key">: {
"empName": Rohit Sharma,
"Designation": SW Developer,
"empId": EMP776,
"branchDetails": {
"branch": Software,
branchLocation: Pune,
branchId: BR556
}
},
<"employee2Key>": {
"empName": Vira tKholi,
"Designation": Accountant,
"empId": EMP908,
"branchDetails": {
"branch": Finance,
branchLocation: Dheli,
branchId: BR557
}
},
<"employee3Key">: {
"empName": MS Dhoni,
"Designation": Sales Manager,
"empId": EMP909,
"branchDetails": {
"branch": Sales and Marketing,
branchLocation: Pune,
branchId: BR556
}
}
}
}
Indexing with orderByChild:
{
"rules": {
"employee": {
".indexOn": ["empName", "empId", "designation"]
}
}
}
Multilevel Indexing:
{
"rules": {
"employee": {
".indexOn": ["empName","empId","branchDetails/branch",
"branchDetails/branchLocation","branchDetails/branchId"]
}
}
}
You can also refer to this link for the Firebase Real-Time Database Data Indexing.
Firebase Real-Time Database Data Indexing