Im having trouble with the security rules for firebase and Im not 100% where I am going wrong. I am thinking that maybe I have my data structure wrong:
{
"users": {
"uid": {
"displayName": "Name";
}
},
"modules": {
"id": {
"title": "buttons",
"uid": "(user id string)"
},
"id": {
"title": "navbars",
"uid": "(user id string)"
}
},
"snippets": {
"id = moduleID": {
"id (of snippet)": "(id string)" {
"uid (user ID)": "(string)",
"body": {
"css": "(some code)",
"html": "(Some code)",
"name": "(string)",
"description": "(string)"
}
}
}
}
Everything in the app works fine, but when I started to add security rules I got access denied errors. Im just wondering if I have the data structure correct in the first place or is the security rules completely wrong?
Security rules:
{
"rules": {
"users": {
"$uid": {
// grants write and read access to the owner of this user account whose uid must exactly match the key ($uid)
".write": "auth != null && auth.uid == $uid",
".read": "auth != null && auth.uid == $uid"
}
},
"snippets": {
"$uid": {
// grants write and read access to the owner of this user account whose uid must exactly match the key ($uid)
".write": "auth != null && auth.uid == $uid",
".read": "auth != null && auth.uid == $uid"
}
},
"modules": {
"$uid": {
// grants write and read access to the owner of this user account whose uid must exactly match the key ($uid)
".write": "auth != null && auth.uid == $uid",
".read": "auth != null && auth.uid == $uid"
}
}
}
Any advice would be greatly appreciated.
It seems the rules are malformed based on the data structure.
The rules have $uid's in each node but your data doesn't match that. Users has uid but modules has id and snippets has id = moduleID.
$uid is a variable that holds the node name so it can be referenced inside { } so you should (for readability) rename that variable in the other two nodes to something that makes more sense inside each {}. Like in modules, have it $module_id.
However. I think the jest of this is you want to limit reading snippets and modules to authenticated users. To do that, you can reference the users node.
a .read rule would be something like this
"modules": {
"$module_id": {
".read": "auth != null && root.child('users/' + auth.id).exists()
}
So your modules node can be read by a user that is auth'd and their uid also appears in the users/ node
Are you using the Firebase Bolt compiler for rules? I had to write some complex rules and doing it by hand gets confusing very quickly.
Below is what it would looks like. Very easy to make changes, compile and try them out.
//current logged in user
isUser(uid) = auth != null && auth.uid == uid;
//does this module id exist
hasValidModule(module_id) = root['modules'][module_id] != null;
//dont let anyone read or write to top node
path / {
read() = false;
write() = false;
}
path /users/$user_id
{
write() = isUser($user_id);
read() = isUser($user_id);
}
path /snippets/$module_id/$snipit_id/$user_id
{
write() = isUser($user_id) && hasValidModule($module_id);
read() = isUser($user_id);
}
path /modules/$user_id
{
write() = isUser($user_id);
read() = isUser($user_id);
}
Here's the json it spits out:
{
"rules": {
"users": {
"$user_id": {
".read": "auth != null && auth.uid == $user_id",
".write": "auth != null && auth.uid == $user_id"
}
},
"snippets": {
"$module_id": {
"$snipit_id": {
"$user_id": {
".read": "auth != null && auth.uid == $user_id",
".write": "auth != null && auth.uid == $user_id && newData.parent().parent().parent().parent().child('modules').child($module_id).val() != null"
}
}
}
},
"modules": {
"$user_id": {
".read": "auth != null && auth.uid == $user_id",
".write": "auth != null && auth.uid == $user_id"
}
}
}
}
There's some info on the Firebase blog but the doc that really helped me is this
https://github.com/firebase/bolt/blob/master/docs/language.md
Related
here is my rules code, the code updates in unity every time the user gets +1 point. the rules need
to make it that points can only be updated by +1 (basically , currentPoints = newPoints +1).
in unity i would write it as:
if (currentPoints < newPoints) currentPoints++;
now how do i state my rules to stick to that so my code cant be hacked/edited by a hacker to give
them +1000 points at a time instead of +1 on the database.
{
"rules": {
"users": {
".read": "auth != null",
"$userId": {
".write": "$userId === auth.uid",
"points": {
".validate": "newData.isNumber()",
".write": "data.val() == null && newData.val() == 1 || newData.val() === data.val() + 1"},
"username": {
".validate": "newData.isString()"}
}
}
}
}
Looking at your current rules, you get caught out by cascading security rules:
{
"rules": {
"users": {
// any logged in user can read another user's data
".read": "auth != null",
"$userId": {
// a user can modify any of their own data
".write": "$userId === auth.uid",
"points": {
// asserts points is a number
".validate": "newData.isNumber()",
// this next rule is ignored when $userId === auth.uid - read/write rules cascade!
// if $userId !== auth.uid, ANYONE can write to this location as long as points is first set to 1 or is increased by 1
".write": "data.val() == null && newData.val() == 1 || newData.val() === data.val() + 1"
},
"username": {
// asserts username is a string
".validate": "newData.isString()"
}
}
}
}
}
With your current rules, as long as I am logged in, I can get the value of /users/someUserId/points, increase it by 1, and then set the new value to /users/someUserId/points. Arguably, the only person who should be able to increase a user's points is the user who owns that score.
To fix this, you need to remove the ".write" rule (which causes this bug) and move it's logic into ".validate":
{
"rules": {
"users": {
// any logged in user can read another user's data
".read": "auth != null",
"$userId": {
// a user can modify any of their own data
".write": "$userId === auth.uid",
"points": {
// asserts points is a number and is correctly increased by 1
".validate": "newData.isNumber() && ((data.val() == null && newData.val() == 1) || newData.val() === data.val() + 1)",
},
"username": {
// asserts username is a string
".validate": "newData.isString()"
}
}
}
}
}
Note: With the ".read": "auth != null" rule for /users, make sure that any private user data, e.g. emails, phone numbers, etc are not under /users - they should be moved into a different tree called /privateUserData (or similar).
This is my DB structure
"tasks"
"$taskId"
...
"user": "firebase user id"
I have already written a rule ".read": data.child('user').val() === auth.uid" under $taskId. When I try to access a single task, this rule is taking effect.
Will this also guarantee that if I write a query like firebase.database().ref('/tasks').orderByChild('status').limitToFirst(1) I'll only get tasks that have user id field as auth.uid. Or should I also write a .read clause under tasks
There are several aspects to be answered in your question:
1/ At which level should you write the security rules?
If you write only at the task level like just follows, you will not be able to query the entire set of tasks.
You can test it by doing the following:
Rules:
{
"rules": {
"tasks": {
"$taskID": {
".read": "auth != null",
".write": "auth != null"
}
}
}
}
JS:
var db = firebase.database();
var ref = db.ref('tasks');
firebase.auth().signInWithEmailAndPassword("....", "....")
.then(function(userCredential) {
ref.once('value').then(function(snapshot) {
snapshot.forEach(function(childSnapshot) {
console.log(childSnapshot.val());
});
});
});
This will fail with "Error: permission_denied at /tasks: Client doesn't have permission to access the desired data."
If you change var ref = db.ref('tasks'); to var ref = db.ref('tasks/123456'); (123456 being an existing task id) you will get a result.
If you change your rules to the following, the two previous queries will work.
{
"rules": {
"tasks": {
".read": "auth != null",
".write": "auth != null"
}
}
}
2/ How should you do to only get tasks that have user id field as auth.uid?
The first point to note is that "Rules are not Filters", as detailed here: https://firebase.google.com/docs/database/security/securing-data#rules_are_not_filters
So if you implement security rules as follows:
{
"rules": {
"tasks": {
"$taskId": {
".read": "auth != null && data.child('user').val() === auth.uid",
".write": "auth != null"
}
}
}
}
You will need to write a query that includes the same restriction on the user uid, like the following:
var db = firebase.database();
firebase.auth().signInWithEmailAndPassword("....", "....")
.then(function(userCredential) {
var ref = db.ref('tasks').orderByChild('user').equalTo(userCredential.user.uid);
ref.once('value').then(function(snapshot) {
snapshot.forEach(function(childSnapshot) {
console.log(childSnapshot.val());
});
});
});
But this query will not work, again, because "Error: permission_denied at /tasks: Client doesn't have permission to access the desired data."
You cannot do the following neither, since "Shallower security rules override rules at deeper paths.":
{
"rules": {
"tasks": {
".read": "auth != null",
".write": "auth != null"
"$taskId": {
".read": "auth != null && data.child('user').val() === auth.uid",
".write": "auth != null"
}
}
}
}
One solution is to use Query-based Rules (see the doc here) and write your rules as follows:
{
"rules": {
"tasks": {
".read": "auth != null &&
query.orderByChild == 'user' &&
query.equalTo == auth.uid",
".write": "auth != null"
}
}
}
However, as you have probably noticed, this will prevent you to order your query (and filter it) by something else than the user (e.g. by status), since "You can only use one order-by method at a time."
The solution would therefore be to create a second data structure in parallel to your existing structure, where you add the user as a top node, like
"tasks"
"$taskId"
...
"user": "firebase user id"
"tasksByUser"
"$userId"
"$taskId"
...
You would use the update() method to write to the two data structures simultaneously. See the doc here.
I gave .read: true under tasks and it is considering the rules written under the individual task objects before returning the results.
I have a data structure as following:
{
"users" : {
"31J59dq1clZ3inHMzoopiigsEg63" : [ {
"name" : "Lanchana",
"timestamp" : 1516916245423,
"uid" : "31J59dq1clZ3inHMzoopiigsEg63",
"userEmail" : "*****#gmail.com",
"total-stars" : 123
} ]
}
}
and fire rules as following:
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid && auth != null",
".write": "!data.exists() || data.child('uid').val() ==
auth.uid && auth != null",
}
}
}
}
I can add new data successfully but I cannot make any updates to the existing one. for ex: If I want to update only total-stars of a particular user based on the uid, how can I specify write and validation rules?
Your data structure seems to have a layer 0 that is missing from the rules. If that layer indeed exists in your actual JSON, you'll want to have it in your rules too:
{
"rules": {
"users": {
"$uid": {
"$postid": {
".read": "$uid === auth.uid && auth != null",
".write": "!data.exists() || data.child('uid').val() ==
auth.uid && auth != null",
}
}
}
}
I called the layer $postid here. You may want to use a name that better reflects what the layer represents.
My current firebase structure looks like the following
Originally, my security looks like
{
"rules": {
"users": {
"$companyId": {
"$userId": {
".read": "auth != null",
".write": "auth != null",
}
}
}
}
}
This is all fine as I can simply get the user detail via
// get user information from database
getUserInformation(companyId: string, uid: string) {
let path = '/users/' + companyId + '/' + uid;
return this.af.database.object(path, { preserveSnapshot: true })
}
However, I want to add more security to this branch such that the user can not update the code in the front end to modify some of these entries via write operation (Read is OK).
I want to have the user the ability to modify these entries based on the isAdmin flag
For example,
companyId (read: auth != null, write: isAdmin == true)
companyName (read: auth != null, write: isAdmin == true)
dailySubmissionLimit (read: auth != null, write: isAdmin == true)
defaultApproverEmail (read: auth != null, write: isAdmin == true)
email (read: auth != null, write: isAdmin == true)
firstName (read: auth != null, write: isAdmin == true)
id (read: auth != null, write: isAdmin == true)
isAccountActive (read: auth != null, write: isAdmin == true)
isAdmin (read: auth != null, write: false)
isPasswordOutdated (read: auth != null, write: auth!=null)
lastLoggedIn (read: auth != null, write: auth!=null)
lastName (read: auth != null, write: isAdmin == true)
passwordChangeDate_epoch (read: auth != null, write: auth!=null)
registerationDate (read: auth != null, write: isAdmin == true)
team (read: auth != null, write: isAdmin == true)
userDefaults (read: auth != null, write: auth!=null)
Since all read = auth != null.. Initially, I thought since I am able to read all the node, I could set the security individual rule for each node as shown below
{
"rules": {
"users": {
"$companyId": {
"$userId": {
"companyId": {
".read": "auth != null",
".write": "(auth != null) && (root.child('users').child($companyId).child(auth.uid).child('isAdmin').val() == true)"
},
"isAdmin": {
".read": "auth != null",
".write": "false"
},
"lastLoggedIn": {
".read": "auth != null",
".write": "auth != null"
},
...
}
}
}
}
}
This however failed when I try to getUserInformation() because I am querying '/users/' + companyId + '/' + uid and the security rule for that particular branch is unknown
I am guessing as an alternative, I could query each node instead '/users/' + companyId + '/' + uid + '/companyId' and combine the data using forkJoin or some sort.
This would be highly ineffecient as I am now calling the database nth time instead of one time.
Another approach that I can think of is to structure the data abit differently by categorising them into three branches "userCanModify", "adminCanModify", "noOnCanModify". Then set security rule for these three nodes as required.
But this seems more like a hack to me as I have to re-structure my data to a very odd format. Also, I will still have to make three request to get the full picture of the user detail
{
"rules": {
"users": {
"$companyId": {
"$userId": {
"userCanModify": {
"lastLoggedIn": {
".read": "auth != null",
".write": "auth != null"
},
...
},
"adminCanModify": {
"companyId": {
".read": "auth != null",
".write": "(auth != null) && (root.child('users').child($companyId).child(auth.uid).child('isAdmin').val() == true)"
},
"companyName": {
".read": "auth != null",
".write": "(auth != null) && (root.child('users').child($companyId).child(auth.uid).child('isAdmin').val() == true)"
},
...
},
"noOneCanModify": {
"isAdmin": {
"read": true,
"write": false
},
...
}
}
}
}
}
}
Is there a much better solution out there that I am missing?
Did you tried this security rule syntax? :
{
"rules": {
"users": {
"$companyId": {
"$userId": {
"$companyIdbis": {
".read": "auth != null",
".write": "root.child('users').child($companyId).child($userId) === auth.uid && data.child('isAdmin').exist()"
},
"isAdmin": {
".read": "auth != null",
".write": "false"
},
"lastLoggedIn": {
".read": "auth != null",
".write": "auth != null"
},
...
}
}
}
}
}
(If I'm not wrong :
data.child('isAdmin').exist()"
should be the equivalent of :
`root.child('users').child($companyId).child($userId).child($companyIdbis).child('isAdmin').exist()"`
[EDIT]:
But I recommend you to have a "flat structure" by separating the Nodes Users and Companies Likes :
Users
userId
compagnyId : true
etc ...
Compagnies
compagnyId
etc ...
This allow you te retrieve a List of users without having to browse all the compagnies nodes
So I have this db structure:
Under profile I want email & provider-name to be readable only for admin and
Username readable for every logged in user.
How I can achieve that?
Here is my rules:
{
"rules":
{
"users":
{
"$uid":
{
// grants write access to the owner of this user account whose uid must exactly match the key ($uid)
".write": "auth !== null && auth.uid === $uid",
"profile":
{
// grants read access only for registered users
".read": "auth !== null",
"email":
{
// This doesn't work with firebase as I was reading doc.
".read": false
}
}
}
}
}
}
So after a bit of research and reading about denormalize structure I guess this way will work. The fact is that I'm tempted to nest, but probably is a bad idea on firebase.
{
"rules":
{
"users":
{
"$uid":
{
// grants write access to the owner of this user account whose uid must exactly match the key ($uid)
".write": "auth !== null && auth.uid == $uid",
"public-profile":
{
// grants read access only for registered users
".read": "auth !== null"
}
}
},
"private-profile":
{
"$uid":
{
".read": "root.child('users').child(auth.uid).child('role').child('admin').val() === 'true' && root.child('users').child('1').child('role').child('admin').val() === 'true'",
".write": "root.child('users').child(auth.uid).child('role').child('admin').val() === 'true' && root.child('users').child('1').child('role').child('admin').val() === 'true'"
}
}
}
}