How to use Firebase Rule exists in SubCollection in Firestore - firebase

I'm currently working on Firebase Rule and I'm also a newbie to it. I am using exists and it seems it doesn't work, probably my syntax or query isn't correct. Any Idea how it should work? I'm trying to check the document data that I encircled in red if it exist.
Edit : It seems I'll be using get() and not exist to extract the uid in the File Path base on the suggestion, my question is how to make the File Path that the field I encircled for me to use it for the condition in Firebase Rule.
Client Code
return Firestore.instance
.collection('contacts')
.document(uid)
.setData({
friendId: {
“message”: {
},
“uid”: friendId,
}
});
Firebase Security Rules
match /databases/{database}/documents {
match /contacts/{userID}/{friendID} {
allow read,write : if isValidToAccessContacts(userID,friendID);
}
}
function isValidToAccessContacts(userId,friendID){
return exists(/databases/$(database)/documents/contacts
/$(request.auth.uid)/$(friendID.data.uid)/{document=**}) ||
exists(/databases/$(database)/documents/contacts
/$(friendID.data.uid)/$(request.auth.uid)/{document=**}) ||
exists(/databases/$(database)/documents/contacts
/$(request.auth.uid)/$(request.auth.uid)/{document=**})
;
}

You can't use a wildcard like this {document=**} using exists. You must call out the full path of the document to check for existence.

Related

Firebase - Rule always denies requests

I am starting to restrict the access to my Firebase Database. So far I have three collections where I simply want to return true for testing purposes.
I did it like this
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /devices/{device} {
allow read, write: if true;
}
match /users/{user} {
allow read, write: if true;
}
match /groups/{group} {
allow read, write: if true;
}
}
}
When I try to test this, I can't access the data no matter what. It gets always denied.
Your rules are correct, I see the issue is how you are using the playground,
In the textbox just enter something like
/devices/yourdeviceId
When you use the console simulator to test your rules, the field for the document to test should only contain the path of the document to get, using only the names of specific collections and document IDs. If you're trying to test document "foo" in collection "devices", then your path should be "/devices/foo". Don't enter the full match path, and don't use any wildcards - you should refere to an actual document in the same way that you would refer to it in your app code.

Firestore security rules get field/id of reference

I have two collections - tenancies and users.
A tenancy doc has a field called "landlordID" and is of type REFERENCE (not String).
Now in my Firestore Security Rules I want to allow a tenancy to be updated ONLY IF the landlordID field of that tenancy matches with the uid of the user making the request, namely request.auth.uid.
Read it as " allow a tenancy document to be updated if the user making the user is authenticated, hence request.auth.uid != null, and the landlordID field's ID should be equal to that of the request.auth.uid.
Hence the code should me something like this:
service cloud.firestore {
match /databases/{database}/documents {
match /tenancies/{tenancyID}{
allow update: if request.auth.uid != null &&
request.auth.uid == get(resource.data.landlordID).id
}
}
I have also tried get(/databases/$(database)/documents/users/$(resource.data.landlordID)).data.id
Supporting screenshot of my database
This should be very simple but get() simply does not work. Firebase Docs, scroll to "Access other documents" was not helpful at all for my situation and I am not sure how to get it working.
It would be a shame if references can't be used like this as they are just like any other field of a document.
Here is a function I made that works for me. I guess you have a user collection with users having the same id as their auth.uid
function isUserRef(field) {
return field in resource.data
&& resource.data[field] == /databases/$(database)/documents/users/$(request.auth.uid)
}
Adjusting to your use case you'd call the function so: isUserRef('landlordID') although the ID at the end of it is a bit misleading as this field is in fact a reference.
I see a couple of issues here. A first problem is that the get() function expects a fully specified ducument path, something like:
get(/databases/$(database)/documents/users/$(resource.data.landlordID)).data.id
A second problem is that you are trying to use the reference type in your rules, I do not think that is possible unfortunately.
The reference type in Firestore is not very helpfull (yet), I think you should store the landlordID as a string, then you can simply do something like:
service cloud.firestore {
match /databases/{database}/documents {
match /tenancies/{tenancyID}{
allow update: if request.auth.uid != resource.data.landlordID;
}
}
I had the same issue I needed an answer for. See this Google-thread with the answer from someone from google. To quote it:
You can get an id out of a path using the "index" operator:
some_document_ref should look like /databases/(default)/documents/foo/bar
which has 5 segments: ["databases", "(default)", ...]
some_document_ref[4] should be "bar"
allow create: if request.resource.data.some_document_ref[4] == "bar";
You can also use the normal get and exists functions on them.
A few difficult aspects of this that you may run into:
There's no way to retrieve the number of segments in a path at the moment (we're adding this soon), so you'll need to know some information about the reference ahead of time
There's not great support for writing references using the simulator in the Firebase Console. I used the Firestore emulator to test out this behavior (gist1, gist2)
might be too late, but I was able to piece together (despite a lack of docs) that a document reference is just a path, and complete path can be created with
/databases/$(database)/documents/users/$(request.auth.uid)
Then I have an array/list in firestore of references, called reads that I can grab with:
get(/databases/$(database)/documents/users/$(userId)/userinfo/granted_users).data.reads
Leaving me able to create a bool, and a rule with:
/databases/$(database)/documents/users/$(request.auth.uid) in get(/databases/$(database)/documents/users/$(userId)/userinfo/granted_users).data.reads
obviously your data structure will vary, but knowing the ref is a path is the important part here.
I had to experiment a little to get this working. Here the function that worked for me
function isUserRef(database, userId) {
return 'user' in resource.data
&& resource.data.user == /databases/$(database)/documents/users/$(userId);
}
And I call it like:
match /answers/{answer} {
allow read:
if isUserRef(database, request.auth.uid);
}
As mentioned by some other answers, a reference has a path property that is just a string that will look something like users/randomuserid123. You can split that into an array and match it against the user making the update request.
...
match /tenancies/{tenancyID}{
allow update: if request.auth.uid != null &&
resource.data.landlordID.path.split('/') == ['users', request.auth.uid]
}
...
Also had a trouble handling this problem, but in my case I needed to allow the user to add a message into a chat only if they're the owner of that chat room. There are 2 "tables" - chats and chat_messages, and chat_messages relate to a specific chat through chatId field. chats objects have ownerId field.
The rule I've used goes like this:
// Allow adding messages into a chat if the user is an owner of the chat room
match /chat_messages/{itemId} {
function isOwner() {
return get(/databases/$(database)/documents/chats/$(request.resource.data.chatId)).data.ownerId == request.auth.uid;
}
allow read: if true;
allow create: if isOwner();
}

Firebase rules - Simulator says yes, code says no

I'm working on a Flutter app using Firebase as a backed. I've set up group based roles in Firebase and the rules simulator in Firebase tells me the user I'm testing has access to the document. When I do a query in my Flutter code, I can see it finds the document and I can see it for a split second before it changes it mind and I get a "Listen for query at students failed: Missing or insufficient permissions." and the document is removed from the snapshot.
The query I use in the Flutter code is as follows:
Firestore.instance.collection('students').where('test', arrayContains: userID).orderBy('name').snapshots()
I have been playing with the document and tried different approaches for the current user to query for the document, and just to test it out I created an array with the userId and look for that.
If I completely skip the rules and just put the "need to be logged in" as requirement then I get a document back but as soon as I use the role based one then it's back to the drawing board. The rules I've set up are:
service cloud.firestore {
match /databases/{database}/documents {
match /students/{student} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
return isSignedIn() && (getRole(rsc) in array);
}
allow read, write : if isOneOfRoles(resource,['teacher', 'student', 'parent']);
}
}
}
Any idea what's causing this?

Firestore security rules with reference fields

I am a bit stuck here as there is no way to debug those rules. I'd appreciate help with below rules.
I want to access:
/modules/module-id/sessions/session-id/parts/
The comparison with null in the first part of hasCompletedPrerequisiteSession() works well, the second part doesn't!
The path /modules/moduleId/sessions/sessionId/prerequisite points to a reference field.
service cloud.firestore {
match /databases/{database}/documents {
function hasCompletedPrerequisiteSession(moduleId,sessionId) {
// this part works well
return getPrerequisiteSession(moduleId,sessionId) == null ||
// !!! this part does not work !!!
hasCompleted(getPrerequisiteSession(moduleId,sessionId).id);
}
function getPrerequisiteSession(moduleId,sessionId) {
return get(/databases/$(database)/documents/modules/$(moduleId)/sessions/$(sessionId)).data.prerequisite;
}
function hasCompleted(sessionId) {
return exists(/databases/$(database)/documents/progress/$(request.auth.uid)/sessions/$(sessionId));
}
match /modules/{moduleId}/sessions/{sessionId}/parts/{partId} {
allow read: if hasCompletedPrerequisiteSession(moduleId,sessionId);
}
}
}
(If I store the session ID as a string instead of a reference to the session, it works fine.)
Edit
Questions
Reference field in security rules. Assuming modules/moduleId/owner points to a field of the type reference. What is the proper way to get the id of the referenced document?get(../modules/moduleId).data.owner.data.id or get(../modules/moduleId).data.owner or something else?
From Firebase support:
It seems that in your use case, you want to get the document name (sessionId) from the value of your reference field (prerequisite), unfortunately, this is not currently supported by Firestore security rules. I would suggest that you store only the sessionId as String on your prerequisite field, or you can also add String field for the sessionId. Keep in mind that the exists() and get() functions only allow you to check if a document exists, or retrieve the document at the given path.
It might be that around getPrerequisiteSession, after using get to pull the object by ref path, you had to use .data first before referencing the id field. Of course, id field needs to be stored as an object field.
For example, in my case I needed to allow user to add a message into a chat only if they're the owner of that chat room. There are 2 "tables" - chats and chat_messages, and chat_messages relate to a specific chat through chatId field. chats objects have ownerId field.
The rule I've used goes like this:
match /chat_messages/{itemId} {
function isOwner() {
return get(/databases/$(database)/documents/chats/$(request.resource.data.chatId)).data.ownerId == request.auth.uid;
}
allow read: if true;
allow create: if isOwner();
}

Basic firestore security rule: exists() working - why is get() not?

I'm trying to validate a read in firestore, based on another document.
The data structure consists of 2 collections: "execution" and "test" .
In the test collection there is only 1 document, with the following id: SRJKCxU4HVDBdB3qyfzG, and theese values:
admin true
id: "SRJKCxU4HVDBdB3qyfzG"
test: 1
My security rules look like this:
service cloud.firestore {
match /databases/{database}/documents {
function testFunction(testId) {
// return exists(/databases/$(database)/documents/test/$(testId));
return get(/databases/$(database)/documents/test/$(testId)).data.id == "SRJKCxU4HVDBdB3qyfzG";
}
match /execution/{exeid} {
allow read, write: if testFunction("SRJKCxU4HVDBdB3qyfzG");
}
}
}
If I use return exists(/databases/$(database)/documents/test/$(testId)); everything work's as expected. But no matter what I can't get this line to work return get(/databases/$(database)/documents/test/$(testId)).data.id == "SRJKCxU4HVDBdB3qyfzG".
I really hope that I am missing simple and obvious? Any help is very common. I have also created a demo firebase project and stackblitz if necessary.
Stackblitz https://stackblitz.com/edit/angular-atfasp
Firebase - send a pm.
Thanks for the help.
Update - temporary solution
Because of a firestore security rule bug, the data property is not populated. This will be fixed. The temporary solution for get() to work is the following:
get(path).data.prop || get(path).prop
resource.id doesn't exist yet, which is why you cannot do this.
According to this Stack Overflow question, there is currently a bug in Firebase security rules:
Because the bug is that data object isn't populated, you should check both properties. There's no ETA for launching a solution on this bug..
The temporary solution is to change the code to this:
return get(/databases/$(database)/documents/test/$(testId)).id == "SRJKCxU4HVDBdB3qyfzG" || get(/databases/$(database)/documents/test/$(testId)).data.id == "SRJKCxU4HVDBdB3qyfzG"

Resources