Compare Arrays / Sets in Firestore Security Rules - firebase

In my usecase a document is worked on by multiple users of a specific group.
The document holds (besides other entries) the two relevant fields for:
ownerUID (String Value - represents the UID of the document owner)
members (Array of Strings - represents the UIDs of all members allowed to work on this document)
I want to ensure (using security rules), that (besides the owner of a document) no member is able to remove another member from the members array - except themselves.
To evaluate this I tried using below function in my Firestore Security Rules
function onlyOwnMemberUIDRemovableOrOwner(){
return request.auth.uid == resource.data.ownerUID ||
resource.data.members.toSet().difference(request.resource.data.members.toSet()) == request.auth.uid.toSet();
}
First statement is pretty obvious and is working fine for other rules in my setup to allow owners to modify documents without restrictions:
request.auth.uid == resource.data.ownerUID
The second statement is causing problems for me. The idea was, to evaluate missing values in the members field by using the .difference() function for sets and to compare it with the set only containing the own UID. Only if the missing UID is the own UID, the function should return true. Unfortunately even when trying to remove the own UID, the statement will still return false.
resource.data.members.toSet().difference(request.resource.data.members.toSet()) == request.auth.uid.toSet()
Are you able to tell me what I was doing wrong here?
Do you know a better approach to solve the problem?
Thank you very much for your support.

Two issues with your code:
toSet() works on list and not on string
You have to make sure ownerUID is not modified
Here is the rule corrected:
function onlyOwnMemberUIDRemovableOrOwner(){
return request.resource.data.ownerUID == resource.data.ownerUID
&& (request.auth.uid == resource.data.ownerUID ||
resource.data.members.toSet().difference(request.resource.data.members.toSet()) == [request.auth.uid].toSet());
}

Related

How do you write a Firebase Cloud Firestore rule to give access to specific collection documents where conditions are met on another collection?

My uidConnections collection is a linked list where the uid is the request.auth.uid and connectedUid == specialEvents.uid.
How do I write a Firebase Cloud Firestore rule where a user can only access specialEvents where the uidConnections.connectedUid == specialEvents.uid and uidConnections.uid == request.auth.uid?
Where I'm currently at...
function isSignedIn() {
return request.auth != null;
}
match /specialEvents/{document=**} {
allow read: if isSignedIn()
&& get(/databases/$(database)/documents/uidConnections).data.uid == request.auth.uid
&& get(/databases/$(database)/documents/uidConnections).data.connectedUid == resource.data.uid;
}
I know that I could simply create an "allowed" list of uid's in specialEvents, but this would be limited to allowed document size. Hence the uidConnections linked list rather than an "allowed" list in specialEvents.
If I correctly understand, you would like, in your security rule to find a document in the uidConnections collection that verifies the different contraints.
With Firestore rules this is not possible, because, as explained the doc "the get() and exists() functions both expect fully specified document paths". In other words, you cannot execute a query in the rules, you need to know upfront the exact reference/path of the document you want to point to.
One solution would be to use the value of connectedUid as the ID of the doc. If you cannot do that you will need to find another approach. The one you mentioned in your question is a possible one. If you think there is a risk of reaching the size limit for one Firestore document, use a (sub)collection.

Firestore rule: working on single document but not a ranged query and why is it?

Here are the two versions of my Firebase rule and in my mind, they should be equivalent:
Version1:
match /transactions/{ts} {
function isOwner() {
return request.auth.uid == get(/databases/$(database)/documents/transactions/$(ts)).data.user;
}
allow read: if isOwner();
}
Version2:
match /transactions/{ts} {
function isOwner() {
return request.auth.uid == resource.data.user;
}
allow read: if isOwner();
}
Now, if I query a single document, such as db.collection('transactions').doc(someIDHere), the two rules works exactly the same in terms of accepting or rejecting.
However, if I am doing a query, like db.collection('transactions').where('user','==',userid).get(), then only version 2 will get through, version 1 will report an error, but I don't know why this is the case. I checked the Firestore document, but there aren't sufficient explanations. Especially, what does the get() method in the rule statement mean and why is it different in range query between using get(absolute path) with using resource.data?
The second rule works because the filter:
where('user','==',userid)
exactly matches the constraints of the rule:
request.auth.uid == resource.data.user
(I added "auth" in there which you were missing - there is no uid property on request.)
Security rules will not perform a get() for each individual document that would match a collection query that could return any number of documents. Firstly, that will not scale for large result sets, and second, there is a limit of only 10 get() per rule evaluation. What security rules will do is check that they query constraints match query for all possible documents that could be matched. In other words, security rules are not filters. The filters on the client must match the constraints on the rules without having to evaluate them for every single document.

Firestore security rules, restricting child/field

What is the equivalent way of Firebase RTDB's newData.hasChildren(['name', 'age', 'gender']) in Firestore? How to restrict the child/field?
Update:
I have updated my question with Firestore rules and explained my issue in detail.
match /{country} {
allow read: if true;
allow create: if isAdministrator()
&& incomingData().countryCode is string
&& incomingData().dialCode is string;
allow update: if (isAdministrator() || isAdminEditor())
&& incomingData().countryCode is string
&& incomingData().dialCode is string;
allow delete: if isAdministrator();
}
create, read and delete is working as expected. But if I try to update using Hashmap with any unmentioned child, it will update without throwing any exception unlike Firebase Database rules, where we mention all the possible childs in newData.hasChildren([]).
What you're doing right now is just checking if two provided field values are strings. You're not requiring that only those to fields exist in the update data. What you can do is use the keys() method of the data map to verify that only certain fields exist in the update. For example, this may work:
request.resource.data.keys().hasOnly(['countryCode', 'dialCode'])
There are a number of other methods available on List objects to help you determine its contents.

How to make complex Firestore rules work fine with maps and lists?

I am currently having a lot of trouble setting up complex Firestore rules, but nothing worked so far and I would love if someone could help.
This is my root database structure:
groups
user_access
meetings
Where user_access has email address as key, and an object/list/value (more on that later) with the groupId he can access followed by the level of permission.
Every meeting has a groupId (where it belong).
So, I would like a rule to: check if the current groupId value from meetings is in the document at user_access with the current email address as key.
I thought about doing this rule (simplified below):
service cloud.firestore {
match /databases/{database}/documents {
match /meetings/{meetingId} {
function correctUser() {
return get(/databases/$(database)/documents/access/$(request.auth.token.email)).data.obj[get(/databases/$(database)/documents/meetings/$(meetingId)).data.groupId] == "leader"
}
allow read: if correctUser();
allow write: if correctUser();
}
}
But it doesn't work.. And I'm not sure why.
I tried making a list:
function correctUser() {
return get(/databases/$(database)/documents/meetings/$(meetingId)).data.groupId in get(/databases/$(database)/documents/access/$(request.auth.token.email)).data.list
}
But it also didn't work and I'm not sure why.
The best case scenario would be using a list of objects (a map), key(id),value(permission). Is that possible? Worst case scenario I can use a list for each different permission, or even put all the ids as value (I'll probably never reach the 20k fields limit).
So, I have two questions:
First, how can I make my rule(s) work?
Second, how do I call values from wildcards from inside fields? For example, on the example above with {meetingId}, how would I use this meetingId as a key? (...).data.meetingId? (...).data[$(meetingId)]? I found it very confusing and bad documented. What about on maps? Same thing?
Thanks!
It took me weeks to find out, but what I ultimately wanted and worked was:
function isLeader() {
return get(/databases/$(database)/documents/access/$(request.auth.token.email)).data[request.resource.data.groupId] == "leader"
|| get(/databases/$(database)/documents/access/$(request.auth.token.email)).data[resource.data.groupId] == "leader"
}
There is difference between request.resource.data.groupId and resource.data.groupId which I didn't know and was killing my requests, sometimes read, sometimes write. Glad it works now.

Firestore security rules based on map values

I want to store if a user is permitted to read a document in the document itself, based on the user's email address. Multiple users should have access to the same document.
According to the documentation Firestore does not allow querying array members. That'S why I'm storing the users email addresses in a String-Bool Map with the email address as a key.
For the following example I'm not using emails as map keys, because it already doesn't work with basic strings.
The database structure looks like that:
lists
list_1
id: String
name: String
owner: E-Mail
type: String
shared:
test: true
All security rules are listed here:
service cloud.firestore {
match /databases/{database}/documents {
match /lists/{listId=**} {
allow read: if resource.data.shared.test == true
}
}
}
Edit: It also doesn't work if I use match /lists/{listId} instead of match /lists/{listId=**}
How I understand it, this security rules should allow reading access to everyone if the value in the map shared[test] is true.
For completness sake: This is the query I'm using (Kotlin on Android):
collection.whereEqualTo("shared.test", true).get()
.addOnCompleteListener(activity, { task ->
if (task.isSuccessful) {
Log.i("FIRESTORE", "Query was successful")
} else {
Log.e("FIRESTORE", "Failed to query existing from Firestore. Error ${task.exception}")
}
})
I'm guessing that I cannot access map values from the security rules. So what would be an alternative solution to my problem?
In the Firestore rules reference it's written that maps can be accessed like that resource.data.property == 'property' so, what am I doing wrong?
Edit: This issue should be fixed now. If you're still seeing it (and are sure it's a bug with the rules evaluator), let me know in the comments.
I've chatted with some folks here about the problem you're encountering, and it appears to be an issue with the security rules itself. Essentially, the problem seems to be specific to evaluating nested fields in queries, like what you're doing.
So, basically, what you're doing should work fine, and you'll need to wait for an update from the Firestore team to make this query work. I'll try to remember to update this answer when that happens. Sorry 'bout that!
Whenever you have (optional) nested properties you should make sure the property exists before continuing to check its' value eg.
allow read: if role in request.auth.token && request.auth.token[role] == true
in your case:
allow read: if test in resource.data.shared && resource.data.shared.test == true
, I was struggling a long time with roles until I realized that on non-admin users the admin field is undefined and firestore rules just crashes and doesn't continue checking other possible matches.
For a user without token.admin, this will always crash no matter if you have other matches that are true eg:
function userHasRole(role) {
return isSignedIn() && request.auth.token[role] == true
}

Resources