I am setting security rules so user can increment a value only if (newData + alreadyInsertedData) < limit
but firebase security rules editor showing below error
Invalid + expression: right operand is not a number or string.
Can we convert newData to int/long so we can add it and check if condition?
the rule I have error in,
".write":"auth != null && ((root.child(\"GameResult\").child($adminRef).child($gameName).child($jodi).child($uid).val()+newData) < root.child(\"Limits\").child($adminRef).child(\"maxAmountOnEachJodi\").val())"
complete rules(please ignore other issues right now),
{
"rules":{
".read":"auth != null",
// ".write":"auth != null",
"GameResult":{
"$adminRef":{
"$gameName":{
"$jodi":{
"$uid":{
".write":"auth != null && ((root.child(\"GameResult\").child($adminRef).child($gameName).child($jodi).child($uid).val()+newData) < root.child(\"Limits\").child($adminRef).child(\"maxAmountOnEachJodi\").val())"
}
}
}
}
}
}
}
database nodes are below:
the value(red marked) in GameResult node should not cross the value(blue marked) in Limit node.
GameResult node:
Limit node:
Finally what I want is:
(GameResult/*/*/*/*:value+ newData) <= (Limits/*/*/maxAmountOnEachJodi:value)
I found it, newData is kind of a object which has a val() function. So calling newData.val() will get the job done.
Related
This article stated that "[writeFields] is now deprecated".
Additionally, I cannot find any documentation for writeFields, it is not even listed as part of Request in the documentation anymore.
Problem
The problem I am facing with Cloud Firestore Security Rules is that verifying that only particular fields are modified requires massive amounts of conditions.
For example, if I want to verify that the only modified value of a document is cakes, I have to write the following rule:
allow update: if request.resource.data.size() == 20
&& request.resource.data.likes == resource.data.likes
&& request.resource.data.name == resource.data.name
&& request.resource.data.date == resource.data.date
&& request.resource.data.body == resource.data.body
&& request.resource.data.title == resource.data.title
&& request.resource.data.tags == resource.data.tags
&& request.resource.data.comments == resource.data.comments
&& request.resource.data.answers == resource.data.answers
&& request.resource.data.awards == resource.data.awards
&& request.resource.data.image == resource.data.image
&& request.resource.data.link == resource.data.link
&& request.resource.data.format == resource.data.format
&& request.resource.data.type == resource.data.type
&& request.resource.data.user == resource.data.user
&& request.resource.data.views == resource.data.views
&& request.resource.data.reports == resource.data.reports
&& request.resource.data.roles == resource.data.roles
&& request.resource.data.category == resource.data.category
&& request.resource.data.votes == resource.data.votes
&& request.resource.data.cakes is int;
Using writeFields, the exact same rule would have looked like this:
allow update: if request.writeFields.hasOnly(['cakes']) && request.resource.data.cakes is int;
What can I do to decrease the code size of my rules / what is the alternative to writeFields?
Limits
There are two limits mentioned in the documentation that make this problem even worse:
Maximum number of expressions evaluated per request: 1,000
Maximum size of a ruleset: 64 KB
I expect to reach both of these at some point with this limitation.
Yes! There is now a replacement called "Map Diffs". Check this syntax out:
allow update: if request.resource.data.diff(resource.data).affectedKeys().hasOnly(['cakes'])
&& request.resource.data.cakes is int;
Unfortunately, what you're doing right now is currently your best option.
The Firebase rules team is working on a language improvement to make it easier to compare/diff map type objects, which will drastically cut down on the number of expressions it takes to do this sort of thing, but there is no timeline for that right now. Please stay tuned.
I end up writing these helper function:
// returns true if all the fields changed are contained in the array parameter
function onlyAllowedFields(fieldsArray) {
return fieldsArray.toSet().hasAll(request.resource.data.diff(resource.data).affectedKeys());
}
// returns true if none of the fields are changed
function noneOfDisallowedFields(fieldsArray) {
return request.resource.data.diff(resource.data).affectedKeys().hasAny(fieldsArray.toSet()) == false
}
I found more helpful this way instead of using hasOnly() that would require that all fields that could be changed must have been changed.
I have these Firestore Security Rules. I need to check if the 3 amounts "redAmount", "greenAmount" and "blackAmount" added together exceed the user's balance. If so, the update should be rejected.
function checkSums() {
return futureDocument().redAmount + futureDocument().blackAmount +
futureDocument().blackAmount <= getUserDoc().amount;
}
But this functions fails if one of the amounts is undefined => not set, which I want to be 0, but I can't. Any ideas on how to fix this?
This problem is not solved by referencing to the "How to validate that a field is undefined" question.
Fixed this with a really insane long workaround (instead of using functions, use or's)
return checkMinusAmount('redAmount') && checkMinusAmount('blackAmount')
&& checkMinusAmount('greenAmount') && (
(futureDocument().redAmount + futureDocument().blackAmount + futureDocument().greenAmount <= getUserDoc().amount)
(futureDocument().redAmount + futureDocument().blackAmount <= getUserDoc().amount !has('greenAmount'))
(futureDocument().redAmount + futureDocument().greenAmount <= getUserDoc().amount !has('blackAmount'))
(futureDocument().greenAmount + futureDocument().blackAmount <= getUserDoc().amount !has('redAmount'))
(futureDocument().greenAmount <= getUserDoc().amount (!has('redAmount') && !has('blackAmount')))
(futureDocument().blackAmount <= getUserDoc().amount (!has('redAmount') && !has('greenAmount')))
(futureDocument().redAmount <= getUserDoc().amount (!has('blackAmount') && !has('greenAmount')))
)
}
I ended up not using it and just creating the document with a cloud function and not giving an user the rights to delete it.
I don't understand how can I set a limit of files upload in a day. I want users to publish a maximum of 10 photos per day. On the database side I put a increment counter. If it reaches a certain size it does not allow a user to post other contents. but by the storage side this is impossible. An attacker can publish all files that he want without limits. Is there a solution to prevent this situation? Thanks in advance. At moment my security rules are:
service firebase.storage {
match /b/projectid/o {
match /Photo/{user}/{photo}/image.jpg {
allow write: if request.auth != null &&
request.auth.uid == user && (
request.resource.size < 5 * 1024 * 1024 && photo.size() < 32 ||
request.resource == null);
allow read: if request.auth != null &&
request.auth.uid == user
}
}
}
Well, there's a very simple way of doing this, and there's the right way of doing this.
The hacky way of only allowing a certain number of files to be uploaded in a certain time period is to name the files with some numerical property: say users/{userid}/0.jpg through users/{userid}/9.jpg (for 10 photos).
You can write a rule to check that as follows:
// Match all filenames like 0.jpg
match /users/{userId}/{photoId} {
allow write: if photoId.matches('^\d\.jpg$')
}
If you need more granularity than order of magnitude, you can do something like:
// Match all filenames like YYY.jpg where YYY is a number less than XXX
match /users/{userId}/{photoId} {
allow write: if int(photoId.split('\.')[0]) < XXX
}
That only solves half our problem though: we can restrict the number of files, but what if a user just wants to upload over them? Luckily, we can write a rule that prevents an end user from overwriting their file ever (though we've got to carve out deletions), or within a given time period. Let's explore:
// Allow files to be overwritten once a day, written if there's nothing there, or deleted as often as desired
match /users/{userId}/{photoId} {
allow write: if request.time > resource.timeCreated + duration.value(1, "d") || resource.size == 0 || request.resource.size == 0
}
These can be combined into function:
function isAllowedPhotoId(photoId) {
return int(photoId.split('\.')[0]) < XXX
}
function canOverwritePhoto() {
return request.time > resource.timeCreated + duration.value(1, "d") || resource.size == 0 || request.resource.size == 0
}
match /users/{userId}/{photoId} {
allow write: if isAllowedPhotoId(photoId) && canOverwritePhoto()
}
Long term, the solution is being able to reference Database data from within Storage, and vice-versa. Unfortunately, that world isn't here yet, but we're working towards it.
So I have some code running an IP check to ensure an ADMIN account cannot have access from outside my network.
string strIP = Request.ServerVariables["REMOTE_ADDR"];
if (
(strIP.Substring(0, 9) != "XXX.XX.X.")
&& (strIP.Substring(0, 10) != "XXX.XX.XX.")
&& (strIP.Substring(0, 6) != "XX.XX.")
&& (strIP.Substring(0, 6) != "XX.XX.")
)
{
..// Check user for being an ADMIN // ....
}
This code was working fine for several weeks, but suddenly has started consistently to err out. The error message is:
Exception
Exception Type: System. ArgumentOUtOfRangeException
Exception Message: Index and length must refer to a location within the string. Parameter name: length.
When I remove the line with "Substring(0,10)", everything works. Also, when I change the line "Substring(0,10)" to "Substring(0,9)" and remove the last ".", everything works.
Can anyone tell me why or perhaps instruct on what is being done incorrectly? For the life of me I can't figure out what is going on.
The problem is that strIP doesn't have 10 characters because your configuration changed for some reason. You could do something like:
(strIP.Length >= 9 && strIP.Substring(0, 9) != "XXX.XX.X.")
|| (strIP.Length >= 10 && strIP.Substring(0, 10) != "XXX.XX.XX.")
|| (strIP.Length >= 6 && strIP.Substring(0, 6) != "XX.XX.")
Notice that the fourth line was a duplicate of the third.
Do not allow an out of bounds error to happen by putting a check for length of strIP before you try to do any of the sub-string comparisons, like this:
if (strIP.Length == 10)
{
if ((strIP.Substring(0, 9) != "XXX.XX.X.")
&& (strIP.Substring(0, 10) != "XXX.XX.XX.")
&& (strIP.Substring(0, 6) != "XX.XX.")
&& (strIP.Substring(0, 6) != "XX.XX."))
{
..// Check user for being an ADMIN // ....
}
}
else
{
// Do something here, error, message to user, deny access, etc.
}
UPDATE:
If you want to only apply checks, based upon the length of the string, then use a switch statement, like this:
switch (strIP.Length)
{
case 6:
if(strIP.Substring(0, 6) != "XX.XX.")
{
// Check user for being an ADMIN
}
break;
case 9:
if(strIP.Substring(0, 9) != "XXX.XX.X.")
{
// Check user for being an ADMIN
}
break;
case 10:
if(strIP.Substring(0, 10) != "XXX.XX.XX.")
{
// Check user for being an ADMIN
}
break;
default:
// IP string is not in format expected, do something here
// Most likely would want to err on the side of caution and deny access
break;
}
How can I enforce a maximum number of children of a node using a security rule?
/bar/customers/$customer/drinks_ordered
Should not have more than fifteen children.
It might seem like you could use a numeric id for the ordered drinks and then try something like this; it will fail since the ID is a string:
"$customer_id": {
"drinks_ordered": {
"$drink_id": {
".validate": "$drink_id > 0 && $drink_id < 16" // error
}
}
}
Instead, you can use a counter and validate the counter to be 1-15, and then validate the drink ID matches the counter.
"$customer_id": {
"counter": {
// counter can only be incremented by 1 each time, must be a number
// and must be <= 15
".validate": "newData.isNumber() && newData.val() > 0 && newData.val() <= 15 && ((!data.exists() && newData.val() === 1) || (newData.val() === data.val()+1))"
},
"drinks_ordered": {
// new record's ID must match the incremented counter
"$drink_id": {
// use .val()+'' because $drink_id is a string and Firebase always uses ===!
".validate": "root.child('bar/customers/'+$customer_id+'/counter').val()+'' == $drink_id"
}
}
}
Naturally, your drinks will look something like this:
/bar/customers/george_thorogood/counter/3
/bar/customers/george_thorogood/drinks_ordered/1/burbon
/bar/customers/george_thorogood/drinks_ordered/2/scotch
/bar/customers/george_thorogood/drinks_ordered/3/beer
Now before a client could add another drink, they would have to set the counter to 4 (which is the only thing it can be set to) and then add the drink with that same ID.
A little roundabout, but it does do the job : )