Firebase db rule to prevent delete related to another node - firebase

Assume that we have two nodes: "items" and "sales". How can I write a firebase db rule to prevent any item being deleted if it is related in another node. If a user wants to delete ("items\i01") it should not give permission because it is a relation under ("sales\s01\i01")
"items": {
"i01": {
"name": "item1"
},
"i02": {
"name": "item2"
},
}
"sales": {
"s01": {
"itemKey": "i01",
"price": "45"
},
"s02": {
"itemKey": "i02",
"price": "60"
},
...
}

Security rules can check whether data exists at a known path, but cannot perform searches for data across (a branch of) the JSON tree. So in your current data structure, there is no way to prevent the deletion of the item based on it still being referenced.
The typical solution would be to add a data structure that you can check in security rules to see if the item is still referenced anywhere. This would pretty much be an inverse of your current sales node, which tracks the items in a sale. The inverse node would track the sales for any item:
"sales_per_item": {
"i01": {
"s01": true
},
"i02": {
"s02": true
}
}
You will need to make sure that this new structure (sometimes called an inverted index) is updated to say in sync with sales, both in code and in security rules.
With that in place, you can then prevent deletion of an item that still has references with:
{
"rules": {
"items": {
"$itemid": {
".write": "!newData.exists() && !newData.parent().parent().child('sales_per_item').child($itemid).exists()"
}
}
}
}
As an alternative, you can consider moving the deletion logic into a Cloud Function, where you can do the "check for orders with the item" in code, instead of in security rules.
I also recommend reading these:
How to write denormalized data in Firebase
Patterns for security with Firebase: combine rules with Cloud Functions for more flexibility
Patterns for security with Firebase: offload client work to Cloud Functions

Related

Authorize access to write and delete only if the user knows the node - Rules Firebase

I face an issue : I want the user of my web program to write and delete data in firebase only if he knows where it is and I don't understand what rules have to apply to that.
For instance, my user has an id which is '785475' and I want to only give him access to '/data/785475'. How do I do that please?
Do I have to use authentification?
Thanks in advance and please have a nice day!
In Firebase security rules permission cascades. So in your model and requirement, it is important that the user doesn't have read access to /data, as that means they could read /data and all nodes under it.
To the trick is to only grant them access to the individual child nodes of /data:
{
"rules": {
"data": {
"$any": {
".write": true
}
}
}
}
With the above nodes, a user can delete any existing node that the know the key of, but they can't read (or query) the entire /data node to determine those keys.
To only allow them to overwrite existing data, and not create any new nodes, you can do:
{
"rules": {
"data": {
"$any": {
".write": "data.exists()"
}
}
}
}
For more on this, and other examples, check out the Firebase documentation on new vs existing data.

Restrict querying by a certain child value in security rules

I'm struggling to come up with the best way to structure part of my database and its associated security rules.
I have chat groups, and users can be added to those groups at any point. When users are added to a group, they should be able to retrieve only the messages sent after that. It shouldn't be possible for them to retrieve any messages that were sent before they (the users) were added to the group.
My first approach wrongly assumed that security rules would apply only to the data being queried.
Simplifying it for this question, I had the following structure:
{
"groups": {
"-Kb9fw20GqapLm_b8JNE": {
"name": "Cool people"
}
},
"groupUsers": {
"-Kb9fw20GqapLm_b8JNE": {
"3JzxHLv4b6TcUBvFL64Tyt8dTXJ2": {
"timeAdded": 1230779183745
},
"S2GMKFPOhVhzZL7q4xAVFIHTmRC3": {
"timeAdded": 1480113719485
}
}
},
"groupMessages": {
"-Kb9fw20GqapLm_b8JNE": {
"-KbKWHv4J4XN22aLMzVa": {
"from": "3JzxHLv4b6TcUBvFL64Tyt8dTXJ2",
"text": "Hello",
"timeSent": "1358491277463"
},
"-KfHxtwef6_S9C5huGLI": {
"from": "S2GMKFPOhVhzZL7q4xAVFIHTmRC3",
"text": "Goodbye",
"timeSent": "1493948817230"
}
}
}
}
And these security rules:
{
"rules": {
"groupMessages": {
".indexOn": "timeSent",
"$groupKey": {
".read": "root.child('groupUsers').child(auth.uid).child($groupKey).child('timeAdded').val() <= data.child('timeSent').val()"
".write": "!data.exists() && root.child('groupUsers').child(auth.uid).child($groupKey).exists() && newData.child('from').val() === auth.uid",
}
}
}
}
With that, I figured I could retrieve the messages for a particular group like so:
var myTimeAdded = /* already retrieved from the database */;
firebase.database()
.ref('groupMessages/-Kb9fw20GqapLm_b8JNE')
.orderByChild('timeSent')
.startAt(myTimeAdded)
.on('child_added', /* ... */);
But like I said, that was a wrong assumption. Any suggestion on how I could achieve this?
Read rules are enforced at the location where you attach a listener.
So in your case that is groupMessages/-Kb9fw20GqapLm_b8JNE. If your user has read permission there the listener is allowed. If the user does not have read permission, the listener is rejected/cancelled.
This means that rules cannot be used to filter data. We often refer to this as "rules are not filters" and it's one of the most common pitfalls for developers who are new to Firebase's security model. See:
the section rules are not filters in the Firebase documentation
previous questions about Firebase that mention "rules are not filters"
By themselves your rules are not wrong: they only allow access to each specific child if it's not too old. They just don't allow you to run a query on groupMessages/-Kb9fw20GqapLm_b8JNE anymore.
The common way to work around this is to have a separate structure (commonly called an "index") with the keys of the items that your query would otherwise return. In your case it looks like that might turn into a index for each user with the keys of all messages after they joined.
But I'll be honest, it sounds like you're trying to use security rules in a SQL way here. It seems unlikely that the user isn't allowed to see older messages. More likely is that you don't want the user to be bother by the older messages. In that case, I'd just solve it with a query (as you already have) and remove the ".read" rule.

Storing User Data Flattened Security

I'd like to create an app that has a lot of user data. Let's say that each user tracks their own time per task. If I were to store it flattened it would look like this:
{
users: {
USER_ID_1: {
name: 'Mat',
tasks: {
TASK_ID_1: true,
TASK_ID_2: true,
...
}
},
},
tasks: {
TASK_ID_1: {
start: 0,
end: 1
},
TASK_ID_2: {
start: 1,
end: 2
}
}
}
Now I'd like to query and get all the task information for the user. Right now the data is small. From their guides: https://www.firebase.com/docs/web/guide/structuring-data.html it says (near the end) "... Until we get into tens of thousands of records..." and then doesn't explain how to handle that.
So my question is as follows. I know we can't do filtering via security, but can I use security to limit what people have access to and then when searching base it off the user id? My structure would then turn to this:
{
users: {
USER_ID_1: {
name: 'Mat'
}
},
tasks: {
TASK_ID_1: {
user: USER_ID_1,
start: 0,
end: 1
},
TASK_ID_2: {
user: USER_ID_1,
start: 1,
end: 2
},
...
}
}
Then I would set up my security rules to only allow each task to be accessed by the user who created it, and my ref query would look like this:
var ref = new Firebase("https://MY_FIREBASE.firebaseio.com/");
$scope.tasks = $firebaseArray(ref.child('tasks/')
.orderByChild('user')
.startAt('USER_ID_1')
.endAt('USER_ID_1'));
Is that how I should structure it? My query works but I'm unsure if it'll work once I introduce security where one user can't see another users tasks.
You've already read that security rules can not be used to filter data. Not even creative data modeling can change that. :-)
To properly secure access to your tasks you'll need something like:
"tasks": {
"$taskid": {
".read": "auth.uid === data.child(user).val()"
}
}
With this each user can only read their own tasks.
But with these rules, your query won't work. At it's most core your query is reading from tasks here:
ref.child('tasks/')...some-filtering...on(...
And since your user does not have read permission on tasks this read operation fails.
If you'd give the user read permission on tasks the read and query would work, but the user could then also read all tasks that you don't want to give them access to.

Firebase permissions based on user's role

In my application I am supposed to have three kinds of roles:
Store: Can do whatever kind of reads and writes on anything bellow their owned key;
Customers: Can create their chat keys only underneath their designated store; Once a chat is created they can only push to the message child, on their own chats;
Attendants: Can only update chats which are designated to them, and insert under their messages tag.
The database is something similar to:
"$store_id": {
"owner": STORE_ID,
"chats": {
...
"$chat_id": {
"owner": COSTUMER,
"attendant": ATTENDANT,
"messages": {
"$message_id": {
"owner": CUSTOMER_ID
}
}
}
...
}
}
I can't figure out how I can achieve this behavior because:
If a permission is applied to a top level node, their children can't override it;
If someone crack into the database with valid credentials (i.e.: customers'), they can assume whatever role they want;
How this kind of issue is managed in Firebase?

Firebase Security API - Complex Data Structure - How to enforce relationships?

For the past few weeks i've been exploring Firebase and its features to build a web app, but I've kind of ran into a wall when it comes to security rules.
I've build a data structure on Firebase but I'm not sure if it follows best practices (if it doesn't, feel free to suggest anything different about it):
{
"groups" : {
<GROUP_KEY>
"name": "",
"rels": {
"users": {
<RELS_USERS_KEY>
"key":"" (USER_KEY)
},
"notes": {
<RELS_NOTES_KEY>
"key":"" (NOTE_KEY)
}
},
"isPrivate": true
},
"users": {
<USER_KEY>
"email": "",
"rels": {
"friends": {
<RELS_FRIENDS_KEY>
"key":"" (USER_KEY)
}
},
},
"notes": {
<NOTE_KEY>
"title": "",
"description": "",
"rels": {
"files": {
<RELS_FILES_KEY>
"key":"" (FILE_KEY)
}
}
},
"files": {
<FILE_KEY>
"mode": "",
"url": ""
}
}
The application flow is as follows:
The user signs up: a key is created on "users";
Is redirected to "Groups" view, where he should be shown only
groups that have his ID in RELS > USERS, or that has
"isPrivate":"false";
As the user creates a Group, a new group is added with his ID in RELS > USERS;
Entering the Group view, he should only see notes that are in RELS > NOTES for that group.
The rest of the logic follows the same principle, and I believe that if I can get through the first hurdle of understanding the Firebase security rules and applying them to this case, I can get through the rest.
I've tried a couple of rules, but I can't seem to get any feedback at all from the web application, debugging this has been a trial-and-error process, and its not really working.
Could someone help me at least understanding the logic behind it ? I've read all of their tutorials but they all seem very shallow with no deeper examples on complex structures.
Thanks for the help.
EDIT
I've added the debug:true flag to the login (thanks #Kato), but I'm still getting no feedback on the rules. With the rules as below, I still enter the "Groups" view, but get no feedback on the console, and the logged-in user sees groups he shouldn't:
{
"rules": {
"groups": {
".read": "data.child('rels').child('users/' + auth.user).exists()",
".write": "data.child('rels').child('users/' + auth.user).exists()"
}
}
}
As for the rules I've tried, they were countless, but this is the most recent one (still no feedback).
Maybe I'm missing something ?
Thanks again.
Rules cascade. That is, if any rule allows read, then you cannot revoke it later in a nested child. In this way, you can write rules like the following:
"$record": {
// I can write the entire record if I own it
".write": "data.child('owner').val() === auth.uid",
"foo": {
// anybody in my friends list can write to foo, but not anything else in $record
".write": "data.parent().child('friends/'+auth.uid).exists()"
},
"bar": {
// this is superfluous as permissions are only "granted" and never "revoked" by a child
".write": false
}
}
Note how, because I am the owner, I can also write to foo and to bar, even though bar has tried to revoke my read privilege.
So in your case above, your rules declaration lists read: true which allows full read access to the entire repo. Change that to false and you'll see better results.

Resources