Security Rules with Firebase - firebase

I have been using Firebase for a little while in a project under development, but haven’t worried too much about security up to now. From now on I would like to implement a few Security rules. I have read the QuickStart tutorial on the subject on Firebase web site, but I am not yet sure how it all fits together.
Here the structure of my data:
myApp
- DataList
- Contents
- randomKey_One
value: "grgrsgs;jj…data…data.."
- randomKey_Two
value: "43efdsd7gs;jj…data…data.."
- randomKey_Three
value: "8dfsvshj…data…data.."
…….
- Names
- randomKey_One
- authorID: "PeterLogID"
- name: "RecordOne_Peter"
- randomKey_Two
- authorID: "JohnLogID"
- name: "RecordStar_byJohn"
- randomKey_Three
- authorID: "PeterLogID"
- name: "RecordTwo_Peter"
…….
There is a one-to-one correspondance between Contents and Names, which is established through the values of randomKey_One, randomKey_Two, ….etc..
Those keys are automatically generated when a new record is created. I store the login ID of the user in the Names section, in the authorID field.
What I want is:
1) To have read access for the whole world to all the data (possibly with the exception of authorIDs).
2) To give write(and delete) access to a record, only if the authorID field matches auth.uid (i.e. the logged in user).
I have already figured out part 1), forgetting the “exception of authorIDs”.
How do I go with part 2)?
What I have tried at this point did not work.
One issue I have is that I don’t know how to access the authorID field within the security rule script, since I do not have the name of its parent.

For those who may one day hit the same problem and read this.
Here I put the solution I came up with, after a few hours. Since this is my first time to deal with Firebase Security Rules, any expert on the subject is welcome to comment.
{
"rules": {
".read": true,
"DataList": {
"Names": {
"$Name": {
".write": "newData.child('authorID').val() === auth.uid || data.child('authorID').val() === auth.uid"
}
},
"Contents": {
"$Content": {
".write": "root.child('DataList/Names/'+$Content).exists() && root.child('DataList/Names/'+$Content).child('authorID').val() === auth.uid"
}
}
}
}
}

Related

Firebase Security - Read with ID

I have users who can create ids with a profile attached like so:
profiles: {
id_1:
name: Bob
id_2:
name: Jim
}
Bob and Jim can sign up and input their special id provided to them. For example, Bob creates an account, with email and password, and also enters his id to see related profile info. His profile is private to him and user that created it.
When Bob signs up, how can I setup my security rules, so that before his account is created (email & password), I can check the id exists? But also keep it private, such that only those with the id can look at the data?
Let me know if I should explain differently.
If I understand your question correctly, you want the general-public unauthenticated user (before sign-up) to be able to read the UIDs, but not any of the data inside those nodes? Unfortunately that's not possible, check out this article which includes the sentence "Firebase rules cascade in such a way that granting a read or write privilege to a parent node always grants that read/write access to all child nodes."
With that said, it isn't impossible to accomplish your end-goal... instead you would need to have an additional publicly readable table that lists all UIDs, and leave the personal data inside the secure profiles table.
As I'm sure you're aware from the Firebase docs, your database rules would then use the format:
{
"rules": {
"profiles": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
},
"otherTable": {
".read": true
}
}
}

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.

Firebase Rules for object only for creator and admin user

I am really confused with Firebase rules and need some help. I googled a lot but actually got only more confused and still don't get it work.
Lets say I have a Firebase db object like this:
root/orders/$id
and inside the $id, I have a child user_id:
root/orders/$id/user_id
Now I want a rule which only allow the user to READ his own orders (but not write anymore in existing once, how ever he need to create new once) and additional I want the users which are admins to READ/WRITE all orders at any time.
I come up with this so far
"orders": {
"$id": {
".read": "root.child('orders').child($id).child('user_id').val() == auth.uid || root.child('users').child(auth.uid).child('admin').val() == 'user_is_admin'",
".write": "root.child('users').child(auth.uid).child('admin').val() == 'user_is_admin'"
},
".write": "root.child('users').child(auth.uid).child('admin').val() == 'user_is_admin'",
".read": "root.child('users').child(auth.uid).child('admin').val() == 'user_is_admin'",
".indexOn": ["status", "user_id"]
},
My admins are marked as admins in my user table:
root/users/$id/admin (== user_is_admin)
My intention was for the first part to allow users with the same auth.uid as the requested /orders/$id/user_id to read their orders and for admins to read and write. The admin part is working, but my user has no access for some reason.
The second part was for admins to have read/write access to all orders (without specific $id) which also works fine plus a normal user need the write to CREATE a new order here.
To resume the admin part of my rules works, but the user part does not.
1. my user cant read is own orders
2. my user cant create a new order
I would be really happy if somebody can help me out here.
The Rules:
The following rules should enforce the policies you've outlined in your question:
"orders": {
"$id": {
".read": "data.child('user_id').val() === auth.uid",
".write": "!data.exists() && newData.child('user_id').val() === auth.uid"
},
".write": "root.child('users').child(auth.uid).child('admin').val() === 'user_is_admin'",
".read": "root.child('users').child(auth.uid).child('admin').val() === 'user_is_admin'",
".indexOn": ["status", "user_id"]
}
Note that Firebase security rules cascade, so once you've granted read/write permissions to admins for orders, you don't need to repeat the rules for orders/$id. (Something to remember - although it's not an issue with your rules - is that once you grant a permission on a parent you cannot revoke it on a child.)
The orders/$id/.read rule uses data.child to compare the user_id and the auth.uid. This is the same as your rule, it's just a little shorter and does not include the admin check (which cascades).
In addition to checking that newData (the value being written) contains the user_id, the orders/$id/.write rule checks to see that data (the previous value) does not exist. That will allow creates, but will deny updates and deletes.
Orders for Users:
As noted in your comment, it's not possible for a user to query a list of orders under the orders key, as the user won't have permission to read the orders of other users (they'd need to be readable to be filtered out). You could solve the problem by storing a mapping of orders by user, like this:
"orders": {
"order_1": {
"user_id": "user_1",
...
},
"order_2": {
"user_id": "user_1",
...
},
"order_3": {
"user_id": "user_2",
...
}
},
"ordersByUser": {
"user_1": {
"order_1": true,
"order_2": true
},
"user_2": {
"order_3": true
}
}
You can use Firebase's multi-location updates to make maintaining the mapping less tedious. For example, to add another order for user_1, you could do this:
firebase.database().ref().update({
"orders/order_4": {
"user_id": "user_1",
...
},
"ordersByUser/user_1/order_4": true
});
This would let users obtain a list of order IDs (and, from those, the orders themselves) and admin users could still obtain a list of all orders, etc.
Also, you should include a rule so that users can only read their own order IDs under ordersByUser, etc.

Firebase add users but disable any modification

got stuck on this problem: I want to allow user to register on my webpage using firebase, for that reason I planned the following structure:
users : {
user0 : { //user specific data },
user1 : { //... }
...
}
Everything works fine (writing, reading...) until I change the security rules. I want to change them because I want the users only to register and not to have the power to delete their or potentially other user accounts. But I couldn't find anything very helpful on that. Below is what I'm currently left with:
{
"rules": {
"users" : {
".read": true,
"$uid" : {
".write": "auth !== null && auth.uid === $uid"
}
}
}
}
I'm wondering how to set up the rules such that users can only add new accounts.
I would appreciate any help!
Thaanks :)
Edit: Here's what I wanted to achieve with the rules above, maybe the below example using the simulator will make my point clear.
The first thing, I want to do is, is to let a user register at the /users node. Therefore, I chose the Custom Auth point in the simulator:
{ provider: 'anonymous', uid: '002a448c-30c0-4b87-a16b-f70dfebe3386' }.
Now, if I choose "simulate write" and give the path /users and the following key-value pair:
{ "002a448c-30c0-4b87-a16b-f70dfebe3386": { "1" : {"a" : "b"}}}
I get the below message (see Result_2), which tells me, that I cannot write to /users because there's no write rule allowing it, which I totally understand in the above security rules configuration, but I don't know how to change them such that I am able to write key-value pairs as the above one while still allowing each user to write on there entry only. I.e. the user with the uid: 002a448c-30c0-4b87-a16b-f70dfebe3386 would be allowed to write on the corresponding value with the rules above as long as he is authenticated (see Result_1).
E.g. Custon Auth authenticated user writing ON HIS ENTRY: (WORKS PERFECTLY AS EXPECTED)
{ provider: 'anonymous', uid: '002a448c-30c0-4b87-a16b-f70dfebe3386' }.
As the previous time. Now, "simulate write" on path:
/users/002a448c-30c0-4b87-a16b-f70dfebe3386
Result_1:
Attempt to write {"4":{"name":"fred"}} to /users/002a448c-30c0-4b87-a16b-f70dfebe3386 with auth={"provider":"anonymous","uid":"002a448c-30c0-4b87-a16b-f70dfebe3386"}
/
/users
/users/002a448c-30c0-4b87-a16b-f70dfebe3386:.write: "auth !== null && auth.uid === $uid" => true
Write was allowed.
Result_2: Writing the user onto the /users nodes fails, i.e. no registering is possible. And I want here to be able to add a user to /users but not be able to modify/delete user from /users. See simulator output below.
Attempt to write {"002a448c-30c0-4b87-a16b-f70dfebe3386":{"1":{"a":"b"}}} to /users with auth={"provider":"anonymous","uid":"002a448c-30c0-4b87-a16b-f70dfebe3386"}
/
/users No .write rule allowed the operation.
Write was denied.
Permissions cascades - once you give a user a permission on /users you cannot remove that permission on /users/$uid anymore.
The solution is to only grant permission on the lowest level, so in your case:
{
"rules": {
"users" : {
".read": true,
"$uid" : {
".write": "auth !== null && auth.uid === $uid"
}
}
}
}

Basic user authentication with records in AngularFire

Having spent literally days trying the different, various recommended ways to do this, I've landed on what I think is the most simple and promising. Also thanks to the kind gents from this SO question: Get the index ID of an item in Firebase AngularFire
Curent setup
Users can log in with email and social networks, so when they create a record, it saves the userId as a sort of foreign key.
Good so far. But I want to create a rule so twitter2934392 cannot read facebook63203497's records.
Off to the security panel
Match the IDs on the backend
Unfortunately, the docs are inconsistent with the method from is firebase user id unique per provider (facebook, twitter, password) which suggest appending the social network to the ID. The docs expect you to create a different rule for each of the login method's ids. Why anyone using >1 login method would want to do that is beyond me.
(From: https://www.firebase.com/docs/security/rule-expressions/auth.html)
So I'll try to match the concatenated auth.provider with auth.id to the record in userId for the respective registry item.
According to the API, this should be as easy as
In my case using $registry instead of $user of course.
{
"rules": {
".read": true,
".write": true,
"registry": {
"$registry": {
".read": "$registry == auth.id"
}
}
}
}
But that won't work, because (see the first image above), AngularFire sets each record under an index value. In the image above, it's 0. Here's where things get complicated.
Also, I can't test anything in the simulator, as I cannot edit {some: 'json'} To even authenticate. The input box rejects any input.
My best guess is the following.
{
"rules": {
".write": true,
"registry": {
"$registry": {
".read": "data.child('userId').val() == (auth.provider + auth.id)"
}
}
}
}
Which both throws authentication errors and simultaneously grants full read access to all users. I'm losing my mind. What am I supposed to do here?
I don't think you want to store user-specific data under a non-user-specific index. Instead of push()ing to your firebase reference, store the user data behind a meaningful key.
e.g.
auth.createUser(email, password, function(error, user) {
if (!error) {
usersRef.child(user.id).set(stuff);
}
});
Now you can actually fetch user data based on who is authenticated.
The custom Auth in the forge's simulator isn't the greatest but if you hit the tab key after selecting the input, it lets you paste or edit the field. At which point you can add {"provider":"facebook","id":"63203497"} or {"provider":"twitter","id":"2934392"} and hopefully get some useful debug out of it.
Assuming your firebase is something like:
{"registry":{
"0":{
"id":"abbacadaba123",
"index":"0",
"name":"New Device",
"userId":"facebook63203497"},
"1":{
"id":"adaba123",
"index":"1",
"name":"Other Device",
"userId":"twitter2934392"}
}
}
This may work for security rules:
{
"rules": {
"registry":{
"$registryId":{
".read":"data.child('userId').val() === (auth.provider + auth.id)",
".write":"(data.child('userId').val() === (auth.provider + auth.id))||(auth != null && !data.exists())",
".validate": "newData.hasChildren(['id', 'index', 'name', 'userId'])",
"id": {
".validate":"newData.isString()"
},
"index": {
".validate":"newData.isNumber()"
},
"name": {
".validate":"newData.isString() && newData.val().length >= 1"
},
"userId": {
".validate":"newData.val() === (auth.provider + auth.id)"
}
}
}
}
}
Your read rule tested as expected. The facebook user read-tests true on registry 0 and false on 1. The twitter user is false on 0 and true on 1.
I did a couple quick tests on the .write and .validate rules and they seem to work.
Hope this helps at least rule out the firebase security rules portion of things, so you can focus on the AngularFire binding part.

Resources