Firestore security rules Multiple get() not working as expected - firebase

These are my firestore security rules:
service cloud.firestore {
match /databases/{database}/documents {
match /collectionA/{someID} {
function checkA() {
return get(/databases/$(database)/documents/collectionA/$(someID)/users/$(request.auth.uid)).data.deleteFlag != true
}
function checkB() {
return get(/databases/$(database)/documents/collectionB/$(request.auth.uid)/companies/$(someID)).data.deleteFlag != true
}
allow read, write: if checkA() || checkB()
}
}
}
And this is my database objects:
project {
collectionA {
companyA {
users {
r9Myn4TfzAVpSZGzyaet {
deleteFlag: false
}
}
}
}
collectionB {
aAzUlfztdYdEIXT3Tva73kCiuy93 {
companies {
companyA{
deleteFlag:false
}
}
}
}
}
And I tried Simulator :
simulation type : get
location : collectionA/companyA
provider : password
Firebase UID : aAzUlfztdYdEIXT3Tva73kCiuy93
I expected this security rules return "true".
Because I thought checkA returned "false" and checkB returned "true".
But they returned "false".
Please let me know if you have any ideas or suggestions.

I heard that the simulator sometimes does not work properly.
I threw the query from the client and verified it and it worked correctly.
So I thought it was a bug in the simulator.
However, I received the following response from Developer Platform Support.
FYI
The get function will get an error if trying to get a value for an
object that does not exist. When an error occurs in the condition
judgment, the security rule is not applied in the security rule.
Because of the specifications of security rules, it is impossible to
control on nonexistent objects

Related

firebase emulator initialization failure on database.rules

When I upgraded to Firebase 8.12.1, the local database emulator gets a Java/JavaScript error parsing the database rules. I trimmed the original rules down to the smallest thing that breaks. Here's what database-debug.log shows, including the JSON input. Bug or did Firebase change some syntax? JSONLint says the JSON itself is OK.
ERROR com.firebase.core.namespace.NamespaceActor - Unexpected error caught in NamespaceActor(...) for AdminEnvelope(SecurityClaims(Some(GCloudToken(owner,GCloudOwner,1602893571,None)),None),UpdateRules({
"rules": {
".read": true,
"tickets": {
"$offering_id": {
"tickets": {
"blocks": {
"$block_id": {
".validate": "newData.hasChildren(['timestamp', 'text']"
}
}
}
}
}
}
}
,false))
java.lang.ExceptionInInitializerError: null
at org.mozilla.javascript.Context.getCurrentContext(Context.java:346)

Is it possible to allow access to a doc if the user is a member of a parent document, or parent's parent, recursively?

Here's my data structure:
folders(collection): {
folderA: {
members: { user123: { role: "author" } }
folders(collection): {
folderB: {
parent(reference): /folders/folderA
members: { userABC: { role: "author" } }
folders(collection): {
folderC: {
parent(reference): /folders/folderA/folders/folderB
fieldA: { content: 'x' }
}
}
}
}
}
}
users: {
user123 :{
name: John
}
userABC :{
name: Bob
}
}
My goal is to allow access to a document by a user that is a member of any parent above the currently requested document. The hope is to allow access to fieldA by both user123 (John) and userABC (Bob) without writing rules specific to folderA, folderB, and folderC. The idea is that potentially there could be folders at least 20 deep.
Even though I know rule functions are limited, this is how I wish I could write the rules for this case:
match /databases/{database}/documents {
match /{path=**} {
allow get: if resource.data.members[request.auth.uid].role == 'author';
allow get: if isParentAuthor(resource);
function isParentAuthor(resource){
if(exists(resource.data.parent) {
if( get(resource.data.parent.path).data.members[request.auth.uid].role == 'author' ) {
return true;
} else {
return isParentAuthor( get(resource.data.parent.path) );
}
} else {
return false
}
}
}
}
Right now I have many levels deep of access to the folders hard coded into my rules. But it would be great to be able to do this recursively. Is that possible? I know that this would create a situation where there could potentially be quite a few document requests, but I think I'm generally okay with that. But maybe there's a more Firebase-y way to pull this off with fewer requests?
There are limitations with security rules that make it impossible to do what you're saying here.
First, there is a limit of 10 document gets per rule invocation. This is a hard limit.
Second, there is no recursion allowed. The rule will immediately fail if a function calls itself.
As suggested in comments, flatten your collections. Put a field in the document to indicate where it's logically nested compared to other documents. Having large collections is not a big deal with Cloud Firestore. You will be able to filter as much as you need with queries.

Allow only one document to be created in Firestore, disallow update, edit etc

I'm writing Quiz app of sorts using VueJS and Firebase firestore.
So far I've made everything except this one last part.
Users are allowed to answer questions without being logged in.
And at the final stage, there is one last question. Everyone can answer this question, but I need to be able to detect who is first.
So far I've tried with checking if answers collection is empty, this works, but response time is the issue and I can reproduce easily two or more users answering at the same time and having message they are the winners.
I'm currently trying with transactions, but cannot figure it out how to catch if document already exists. Here's the sample code:
let vm = this;
fsdb.runTransaction(function (transaction) {
let voteDocRef = fsdb.collection('final_vote').doc('vote');
return transaction.get(voteDocRef).then(function (voteDoc) {
if (!voteDoc.exists) {
voteDocRef.set({voted: true}).then(function () {
vm.trueAnswer(index);
return 'set executed!';
}).catch(function () {
vm.falseAnswer(index);
throw 'Someone already voted!';
});
return 'Document created!';
} else {
throw 'Someone already voted!';
}
});
vm.trueAnswer and vm.falseAnswer are the methods I'm using to show the popup message.
And this is the method that's triggered once the user submits the answer.
At first I've tried with rules that everyone can read, write ... but now I'm trying to limit write only if document doesn't exist. Here's the current rule set:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /{collectionName}/{docId} {
allow create: if collectionName == 'final_vote' && docId == 'vote';
}
}
}
So far this doesn't work as expected.
Any ideas on how to approach this?
Would you try the following code?
let vm = this;
fsdb.runTransaction(function (transaction) {
let voteDocRef = fsdb.collection('final_vote').doc('vote');
return transaction.get(voteDocRef).then(function (voteDoc) {
if (voteDoc.data().voted !== true) {
voteDocRef.set({voted: true}).then(function () {
vm.trueAnswer(index);
return 'set executed!';
}).catch(function () {
vm.falseAnswer(index);
throw 'Someone already voted!';
});
return 'Document created!';
} else {
throw 'Someone already voted!';
}
});
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /final_vote/{docId} {
allow create, update: if true;
}
}
}
I think that final_votes is better than final_vote as collection name.

Firebase: allow anyone from an array of UIDs to access read / write

I'm developing an app using Firebase's Realtime Database and need to allow multiple users to access the same data, but I'm having trouble figuring out a security rule that makes this work.
The database looks like this:
teams: {
teamID3ic3kic9w3jkck : {
userIDs: ["11111", "22222", "33333", "44444"]
teamData { ....}
}
}
where I want to allow users with an ID matching any of the IDs in the "userIDs" array to access "teamData". Would really appreciate help figuring this out.
Every time you're looking to do array.contains(), you're likely using the wrong data structure. For example, this seems more like a mathematical set to me: an unordered collection of unique items. In Firebase you'd model that as:
teams: {
teamID3ic3kic9w3jkck : {
userIDs: {
"11111": true,
"22222": true,
"33333": true,
"44444: true"
]
teamData { ....}
}
}
Now you can secure this with:
{
"rules": {
"teams": {
"$teamid": {
".read": {
".read": "data.child('userIDs').child(auth.uid).exists()"
}
}
}
}
}

How can you use Firebase rules to allow user and admin write privileges?

I am using Firebase rules to set permissions and I am having trouble setting up rule to allow write permissions to include the user and any admin.
Below is the database I am trying to set permissions for:
'rules': {
'rankings': {
'user': {
'userShortenedAuthId': { //user's rankings },
'anotherShortenedAuthId': { //another user's rankings }
}
},
'admin': {
'adminAuthId': true
},
'users': {
'userAuthId': {
'rank': 'userShortenedAuthId'
},
'anotherAuthId': {
'rank': 'anotherShortenedAuthId'
}
}
}
These are the rules in place for this data:
"rankings":{
"user":{
"$rankId": {
".read": true,
".write": "auth != null && ((root.child('users/' + auth.uid + '/rank').val() == $rankId) || root.child('admin/' + auth.uid).exists())"
}
}
}
I am trying to write to a 'rankings/user/userShortenedId' while logged in under the admin, but I get a permission denied error from Firebase. (Logged in as the user works just fine). The error is somewhere in the 'rankings/user/$rankId/.write' rule. What is confusing for me, is I have been using the 'root.child('admin/' + auth.uid).exists())' rule elsewhere for admin privileges and that seems to work fine too.
Here is the code that produces the permission error.
firebase.database().ref('rankings/' + userShortenedAuthId).update({newPlayerKey:totalPlayers}))
.then(function(){
console.log('successfully updated user ranking');
})
.catch(function(err){
console.log('error updated user ranking', err);
});
Found the answer to my own question. The firebase ref URL that was referenced when trying to update
firebase.database().ref('rankings/' + userShortenedAuthId).update({newPlayerKey:totalPlayers}))
should actually be
firebase.database().ref('rankings/user/' + userShortenedAuthId).update({newPlayerKey:totalPlayers}))
So the rules were actually working correctly. I was simply trying to write to the wrong place.

Resources