Perhaps I'm tackling this problem too much from an SQL kind of perspective, but I'm having troubles understanding how to properly restrict which children should be allowed to populate a node.
Say that I want to keep a record of products with arbitrary names. Each product must contain a price, but nothing else is allowed.
My naive approach was to add a .validate rule to the products requiring newData to contain a price child, explicitly granting write access to the price node and then removing all access an $other node (somewhat like a default clause in a switch statement):
{
"rules": {
"$product": {
".read": true,
".write": true,
".validate": "newData.hasChildren(['price'])",
"price": {
".write": true,
".validate": "newData.isNumber()"
},
"$other": {
".read.": false,
".write": false,
}
}
}
}
This does not work. Adding a new product with {"price": 1234, "foo": "bar"} will still be accepted. If I however add a ".validate": false rule to $other, nothing is accepted instead (e.g. {"price": 1234} is not allowed). (I did that wrong, somehow.)
Is there some way to implement something similar to what I'm trying to do here? If not, what is the proper way of restricting a data structure in Firebase? Should I do it at all? What stops the user from filling my database with trash if I don't?
You're falling into a few common Firebase security pits here. The most common one is that permission cascades down: once you've granted read or write permission on a certain level in the tree, you cannot take that permission away at a lower level.
That means that these rules are ineffectual (since you've granted read/write one level higher already):
"$other": {
".read.": false,
".write": false,
}
To solve the problem you must realize that .validate rules are different: data is only considered valid when all validation rules are met. So you can reject the $other data with a validation rules:
{
"rules": {
"$product": {
".read": true,
".write": true,
".validate": "newData.hasChildren(['price'])",
"price": {
".validate": "newData.isNumber()"
},
"$other": {
".validate": false
}
}
}
}
Related
I have a Firebase database that I want to only allow users who have access to that application to be able to read from and write to.
My data structure is like so:
{
"applications": {
"id_1": {
"feature": {
"a": true,
"b": false
},
"users": {
"user_id_1": true,
"user_id_2": true
}
}
}
}
As you can see, each application can have many users who have read/write access.
I only want users in the users object to be able to retrieve that application.
I have rules like so:
{
"rules": {
"applications": {
".read": "auth != null",
".write": "auth != null",
"$appId": {
".write": "data.child('users').child(auth.uid).val() === true",
".read": "data.child('users').child(auth.uid).val() === true"
}
}
}
}
".read": "auth != null", allows any user who is logged in to be able to retrieve all applications. I only want users user_id_1 or user_id_2 to be able to read that application.
In pseudo code, I would do something like this:
{
"rules": {
"applications": {
".read": "only users in `root.applications.$appId.users` can read", // I need to replace `$appId` some how
".write": "auth != null",
"$appId": {
".write": "data.child('users').child(auth.uid).val() === true",
".read": "data.child('users').child(auth.uid).val() === true"
}
}
}
}
How can I restrict it so when user user_id_1 fetches their applications, they only see apps they have access to?
You're hitting a few common problems here, so let's go through them one by one.
1. Security rules can't filter data
You're trying to control in your rule on /applications what application will be returned when a user tries to read that node. Unfortunately that is not possible, because security rules grant all-or-nothing access.
So either the user has access to /applications (and all data under it), or they don't have access to it. You can't set a rule on /applications to grant them access to some child nodes.
In the documentation, this is referred to as rules are not filters and the fact that permission cascades.
2. Avoid nesting data
You're trying to grant access to /applications, but then store two types of data under there for each application. In cases like that, it is usually better to store each type of data as its own top-level list.
So in your case, that'd be:
{
"application_features": {
"id_1": {
"a": true,
"b": false
},
},
"application_users": {
"id_1": {
"user_id_1": true,
"user_id_2": true
}
}
}
This allows you to grant separate access permissions for the application users and its features. While it means you'll have to read from both branches to get all information of each user, the performance difference there is negligible as Firebase pipelines those requests over a single socket
For controlling access and the most scalable data structure, Firebase recommends that you avoid nesting data and flatten your data structure.
3. Model the data in your database to reflect the screens of your app
Since granting anyone access on /applications gives them access to all data under that, you'll likely need another place to store the list of applications for each user.
I usually make this list explicit in my databases, as another top-level list:
{
...
"user_applications": {
"user_id_1": {
"id_1": true
},
"user_id_2": {
"id_1": true
}
}
}
So now when you want to show the list of applications for the current user, you load the IDs from /user_applications/$uid and then look up the additional information for each app with extra calls (which in turn can be pipelined again).
This one is not in the documentation, but a common pattern with NoSQL databases. I recommend checking out my answers to Many to Many relationship in Firebase and Firebase query if child of child contains a value.
I want to do a few nested if statements to achieve that every user's email is verified befor he gets read write access.
But I dont know how to do that because if I write it in
"rules" : {
"read" : "auth.token.email_verified",
"write" : "auth.token.email_verified"
"other_location" :{
"read" : true,
"write": false
}
}
The user will get write in other_location even through I set write to false in other_location.
I don't know why its like that but my simulation showed that.
Can someone help me?
Firebase RTDB Rules cascade from higher tiers down to the more specific ones. If you allow the ability to perform read/write operations on a given key, which in your case is the root of your database, any key nested under that one will share the same allowed read/write permissions even if your nested rule says otherwise.
To overcome this, you can use variables in the key's path to match unnamed locations. By convention, these "other keys" variables are called "$other".
{
"rules": {
"restricted-location": {
".read": true,
".write": false
},
"$other": { // any key not named above at this level
".read": "auth.token.email_verified",
".write": "auth.token.email_verified"
}
}
}
I try to get my firebase realtime database rules running correct but have a problem with a single property rule.
My firebase object looks like this example
"products": {
"KUg68BknfYWuEjAKla5": {
"cat": "Pizzas",
"likes": 132,
"item_price": 39.9,
"item_price_single": 39.9,
"name": "Mussarela",
"observation": "",
"product_id": "-KUg68BknfYWuEjAKla5",
"product_image": "massapan-mussarela.jpg",
"quantity": 1
}
My rules for this object look right now like this
"products": {
".read": true,
".write": "root.child('users').child(auth.uid).child('admin').val() == 'user_is_admin'"
".indexOn": ["status", "categoryId"]
},
So basically I allow everybody to read the object but only the admin to write the object. My problem is that the single property "likes" need to be writeable by every authenticated user. Which I would normally do with ".read": "auth != null", but I dont know how to combine them in my rules. Can I set multiple lines with .write? do I need to combine them in one line? I tried all I can think of but without success.
thx in advance
You can specify access to specific child nodes within the rules. For example
products
product_0
likes: 123
item_price: 1.99
product_1
likes: 222
item_price: 4.99
rules that would only allow reading the likes node by all but limit writing to the admin would look something like this (not tested but something along these lines)
{
"rules": {
"products": {
"$each_product": {
"likes": {
".read": true,
".write": "root.child('users').child(auth.uid).child('admin').val() == 'user_is_admin'"
},
"item_price": {
".read": true,
".write": true
}
}
}
}
}
On the other hand the item_price node could be written to and read by all. None of the other child nodes would be accessible by anyone.
Instead of storing the amount of likes, you could make a list of users who have liked that category/item. Something like this:
likes: {
userid_1: true,
userid_2: true
}
That way, you can allow users to edit only their path. Just like you'd normally do with a user list:
".write": "auth.uid === $user"
The true value can be anything really, as users who didn't like the content wont be in the list.
You'll just need to count the number of items in the list to get the number of likes.
And no, you can't have multiple write rules. Instead, use ".validate". Read and write rules cascade, so if one is true, all the others will be ignored. Validate rules don't cascade, they all need to be true
I've been wrestling with Firebase security for a little while now and am not having much luck with a scenario that I don't think is very unique (but it is also not covered in the documentation).
Imagine I have a tree, test_tree, which is at the root of my Firebase database. From there, I have three children under test_tree named requiredString1, requiredString2, and optionalString1.
I would like for an authenticated user of the Firebase to be able to write to test_tree, and require that both of the requiredString children are included, while allowing optionalString1 to be optional. There is one additional caveat which is throwing me for a loop -- although optionalString1 is optional, it should not be allowed to be deleted.
So, with those requirements in mind, I've come up with the following security rules:
"rules" {
"test_tree": {
//Define overall write rules
".write": "
auth !== null &&
newData.exists() //this is done to ensure that a deletion of this tree cannot occur
",
".validate": "
newData.hasChildren(['requiredString1', 'requiredString2'])
",
//Define rules for each child
"requiredString1":{
".validate": "
newData.isString()
"
},
"requiredString2":{
".validate": "
newData.isString()
"
},
"optionalString1":{
".validate": "
newData.isString()
"
},
//And finally, ensure no other miscellaneous children can be written
"$other": {
".validate": false
}
},
//Also, ensure lockdown on all other root trees
"$other": {
".read": false,
".write": false,
".validate": false
}
}
I've started to put together a test suite to test my rules as I go along, but the optional-yet-not-delete rule is causing problems.
With the above rules, I fail on two tests:
A write to test_tree with following payload is allowed (this needs to fail).
{
requiredString1: "string1",
requiredString2: "string2",
optionalString1: null
}
A write to test_tree/optionalString1 with payload of null is allowed (this needs to fail).
I've tried to get tricky with my validation rules, such as:
"rules": {
...
".validate": "
//Ensure that required values are present
newData.hasChildren(['requiredString1', 'requiredString2']) &&
(
//IF optionalString is included, ensure that it's not null
(
newData.hasChild('optionalString1') &&
newData.hasChild('optionalString1').val() !== null
)
||
//But also allow it to be non-present
!newData.hasChild('optionalString1')
)
"
...
}
But, unfortunately, this results in the same errors as before.
I've tried some other rule structures as well including actually moving the entire rule set into each child location (and removing the .write and .validate rules at the parent test_tree location), but then writes to the parent location (that previously worked) would then fail.
Some help here? Again, I would think that allowing data to be optional, yet still prevent deletion, would be a common need.
EDIT 1:
I've spent some time thinking about my question, and I think the requirement is a little bit of a misnomer. Basically, what I was asking for was that the data be optional if and only if it's not present in the Firebase database. If it is present in the Firebase, it essentially becomes required.
However, once I realized that was the true requirement, it made describing the use case easier. Hopefully, it'll be clear in a moment, but basically the use case is protecting the developer that is interfacing with Firebase from themselves.
Imagine there is a tree of several descriptors for an item, and the tree looks like the following:
items: {
item_ID1: {
name: "Item Name",
descriptorA: "A descriptor",
descriptorB: "Another descriptor"
}
}
Basically, I was thinking that descriptorA and descriptorB would be optional, and the name would be required. In the event that name needed to be changed whereas descriptorA and descriptorB would remain the same, I wanted to protect the developer that is writing the interface to this data from being able to accidentally blast descriptorA and descriptorB by using a .set({item_ID1: {name: "New Name"}}).
I think this actually can be accomplished with the following rules:
"rules": {
"items": {
".write": "
auth !== null &&
newData.exists()
",
".validate": "
newData.hasChild('name') &&
(
!data.hasChild('descriptorA') ||
(data.hasChild('descriptorA') && newData.hasChild('descriptorA'))
) &&
(
!data.hasChild('descriptorB') ||
(data.hasChild('descriptorB') && newData.hasChild('descriptorB'))
)
",
"name": {
".validate": "newData.isString()"
",
"descriptorA": {
".validate": "newData.isString()"
",
"descriptorB": {
".validate": "newData.isString()"
",
"$others": {
".validate": false
}
}
}
The first problem is (as you said) that .validate rules are not executed if there is no new data. So you'll need to detect the condition in a .write rule.
The second problem (as you also said) is that "rules cascade", so if you give allow an operation on a higher level node, you cannot take it away on a lower level. For that reason, you'll need to detect the condition on a higher level in the JSON structure.
So the solution is to use a .write rule, higher in the JSON tree.
"test_tree": {
".write": "newData.exists() && (newData.hasChild('optionalString') || !data.hasChild('optionalString'))",
".validate": "newData.hasChildren(['requiredString'])",
"requiredString": {
".validate": "newData.isString()"
},
"optionalString":{
".validate": "newData.isString() || !data.exists()"
},
"$other": {
".validate": false
}
I've simplified your data structure a bit, to only include a single required and a single optional property.
This is all described in EDIT 1 above.
I've spent some time thinking about my question, and I think the requirement is a little bit of a misnomer. Basically, what I was asking for was that the data be optional if and only if it's not present in the Firebase database. If it is present in the Firebase, it essentially becomes required.
However, once I realized that was the true requirement, it made describing the use case easier. Hopefully, it'll be clear in a moment, but basically the use case is protecting the developer that is interfacing with Firebase from themselves.
Imagine there is a tree of several descriptors for an item, and the tree looks like the following:
items: {
item_ID1: {
name: "Item Name",
descriptorA: "A descriptor",
descriptorB: "Another descriptor"
}
}
Basically, I was thinking that descriptorA and descriptorB would be optional, and the name would be required. In the event that name needed to be changed whereas descriptorA and descriptorB would remain the same, I wanted to protect the developer that is writing the interface to this data from being able to accidentally blast descriptorA and descriptorB by using a .set({item_ID1: {name: "New Name"}}).
I think this actually can be accomplished with the following rules:
"rules": {
"items": {
".write": "
auth !== null &&
newData.exists()
",
".validate": "
newData.hasChild('name') &&
(
!data.hasChild('descriptorA') ||
(data.hasChild('descriptorA') && newData.hasChild('descriptorA'))
) &&
(
!data.hasChild('descriptorB') ||
(data.hasChild('descriptorB') && newData.hasChild('descriptorB'))
)
",
"name": {
".validate": "newData.isString()"
",
"descriptorA": {
".validate": "newData.isString()"
",
"descriptorB": {
".validate": "newData.isString()"
",
"$others": {
".validate": false
}
}
}
I am building a simple Firebase application with AngularJS. This app authenticates users through Google. Each user has a list of books. Anyone can see books, even if they are not authenticated. Only the creator of a book can edit it. However, individual users need to be able to record that they've read a book even if someone else added it.
I have rules.json like so:
{
"rules": {
".read": false,
".write": false,
"book": {
"$uid": {
".write": "auth !== null && auth.uid === $uid",
}
".read": true,
}
}
}
And I am trying to write a book simply with:
$firebaseArray(new Firebase(URL + "/book")).$add({foo: "bar"})
I get a "permission denied" error when trying to do this although I do seem to be able to read books I create manually in Forge.
I also think that the best way to store readers would be to make it a property of the book (a set of $uid for logged-in readers). ".write" seems like it would block this, so how would I do that?
"$uid": {
".write": "auth !== null && auth.uid === $uid",
"readers": {
".write": "auth !== null"
}
},
It seems like a validation rule would be appropriate here as well ... something like newData.val() == auth.uid, but I'm not sure how to validate that readers is supposed to be an array (or specifically a set) of these values.
Let's start with a sample JSON snippet:
"book": {
"-JRHTHaIs-jNPLXOQivY": { //this is the generated unique id
"title": "Structuring Data",
"url": "https://www.firebase.com/docs/web/guide/structuring-data.html",
"creator": "twiter:4916627"
},
"-JRHTHaKuITFIhnj02kE": {
"title": "Securing Your Data",
"url": "https://www.firebase.com/docs/security/guide/securing-data.html",
"creator": "twiter:209103"
}
}
So this is a list with two links to articles. Each link was added by a different user, who is identified by creator. The value of creator is a uid, which is a value that Firebase Authentication provides and that is available in your security rules under auth.uid.
I'll split your rule into two parts here:
{
"rules": {
".read": false,
"book": {
".read": true,
}
}
}
As far as I see your .read rule is correct, since your ref is to the /book node.
$firebaseArray(new Firebase(URL + "/book"))
Note that the ref below would not work, since you don't have read-access to the top-level node.
$firebaseArray(new Firebase(URL))
Now for the .write rules. First off is that you'll need to grant users write-access on the book level already. Calling $add means that you're adding a node under that level, so write-access is required.
{
"rules": {
"book": {
".write": "auth != null"
}
}
}
I leave the .read rules out here for clarity.
This allows any authenticated user to write to the book node. This means that they can add new books (which you want) and change existing books (which you don't want).
Your last requirement is most tricky. Any user can add a book. But once someone added a book, only that person can modify it. In Firebase's Security Rules, you'd model that like:
{
"rules": {
"book": {
".write": "auth != null",
"$bookid": {
".write": "!data.exists() || auth.uid == data.child('creator').val()"
}
}
}
}
In this last rule, we allow writing of a specific book if either there is no current data in this location (i.e. it's a new book) or if the data was created by the current user.
In the above example $bookid is just a variable name. The important thing is that the rule under it is applied to every book. If needed we could use $bookid in our rules and it would hold -JRHTHaIs-jNPLXOQivY or -JRHTHaKuITFIhnj02kE respectively. But in this case, that is not needed.
First off the "permission denied" error. You are getting this error because you are trying to write directly in the "book" node instead of "book/$uid".
Example of what you do now:
"book": {
"-JRHTHaIs-jNPLXOQivY": { //this is the generated unique id
"foo": "bar"
},
"-JRHTHaKuITFIhnj02kE": {
"foo": "bar"
}
}
In your rules you have a global rule for write set to false so that will be the default and next to that you have made a rule for the specific node book/$uid. So when trying to write directly in "book" it will take the default rule that was set to false. Have a look at Securing your data for more information about firebase rules.
And for the last part of your question i suggest you take a look at Structuring data for more information about the best ways to structure your data inside firebase.
So taka a good look at what and how you want to save and write in firebase and make sure your rules are structured accordingly.