check for absent field with firestore security rules - google-cloud-firestore

I am running a cloud function triggered by an onCreate event. This function required the userID, so after reading this post:
https://stackoverflow.com/a/50842161/4484332
.. i am passing the userId in the created document.
The cloud function is then deleting the userId field.
Now, since I read that it can take up to 10s for the cloud function to run, I want to make sure that the document is not queried before userId is deleted.
function isAdmin(){
return request.auth.uid == "***(admin's uid)***"
}
match /messages/{message} {
allow create: if request.auth.uid != null &&
(isNewMessage(request.resource.data)||isAdmin()) &&
userExists() && (matchesParent()||isFirstChild()||isSeed()||isAdmin());
allow read: if resource.data.userId == null || resource.data.userId == request.auth.uid
allow update: if isAdmin();
allow delete: if isAdmin();
}
The problem is the allow readline: I get FirebaseError:
Property userId is undefined on object.
Client query:
await db
.collection("messages")
.where("subcategoryId", "==", subcategorie)
.where("rank", "==", 0)
.orderBy(value, order)
.limit(paginationNumber)
.startAfter(last)
.get();
Edit: Rules are not filters and it looks like it is what I am trying to do..
Maybe the whole approach to this problem is wrong and my mistake is that I use the firebase authentication uid as document id for each user in the 'users' collection, including the admin user. So I am reluctant in having the admin's uid out there for 10s before the cloud function deletes the userId field..

What you're trying to do isn't possible because security rules are not filters. Please read that documentation carefully - your rule is apparently trying to be a filter.
Security rules can't filter out documents from a query. Either the query returns all of the matching documents, or it generates an error. The query must specify its own filters, and those filters must match what is required by the rules.
If you want to use resource.data in a rule, that can only work for individual document get(), but never for queries.

1) Instead of having your Cloud Function deleting the field, just set it to a dummy value meaning it has been removed.
2) Add a condition on the userid in your where statement, in addition to the rules
await db
.collection("messages")
.where("userid", "==", 0) // Dummy value, use whatever that will never match a real userid
.where("subcategoryId", "==", subcategorie)
.where("rank", "==", 0)
.orderBy(value, order)
.limit(paginationNumber)
.startAfter(last)
.get();
You have to use a dummy value because Firestore does not allow to filter for something that does not exist

Related

Firestore security rules based on constraint values

When using queries the resource variable points to the actual query being made.
How can we access the query arguments in firestore rules and apply restrictions based on the query arguments ?
in this specific case I want to know if the query uses a filter that begins with uid of the currently authenticated user.
the query I use is where('tags', 'array-contains', ${context.user.id}_${context.month})
and using debug(resource.data.tags) in firestore rules logs:
constraint_value {
simple_constraints {
comparator: LIST_CONTAINS
value {
string_value: "EqrtNecgmGWVdLOqOmacFRE6uDef_1659312000"
}
}
}
Edit:
document structure is:
{
creatorId: <uid>
tags:['<uid>_<month>',....]
data: .....
}
I can obtain the required behavior using this query:
.where('creatorId','==',<uid>).where('tags','array-contains',<uid>_<month>)
and this function to validate
function isDocCreator(){
return request.auth.uid.matches(resource.data.creatorId)
}
however this will require an index to be created and the whole purpose of creating the tags attribute goes to waste
It would be nice to be able to access the constaint values by index and get rid of unnecessary index
The only property you can check from the query itself is its limit clause)
What you can do in the rules is say that the user has access to all documents where a field starts with a certain value:
allow read: if resource.data.field >= request.auth.uid
&& resource.data.field <= request.auth.uid + '~';
If a user then creates a query on that same with field a condition, the rules can validate that they're not trying to get any documents they don't have access to, and the query is allowed.
But I'm not sure if there's a way to rewrite the above condition for array members as there are no operation to loop over an array/list in the rules.

cloud_firestore/permission-denied with flutter

I have this document in the firestore cloud :
and I want to get download it in the app by this request :
QuerySnapshot<Map<String, dynamic>> value = await FirebaseFirestore
.instance
.collection('Notification')
.where("ChatID", isEqualTo: 'UyqfawuqBG0km69E2aY8')
.get()
and I have this rules :
allow delete,read:if request.auth != null
&& (resource.data.SenderID == request.auth.uid || request.auth.uid in resource.data.Receivers);
and I get the error : [cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation.
I don't know why ?!!
Firebase security rules in don't filter data requests, as that wouldn't scale. Instead, all they do is ensure that the operations don't access more data than they are allowed to access.
This means that you'll need to replicate the access conditions from your security rules in the query that your code runs. So in your case that means you need to have a query that only requests documents where the current user's UID matches either the sender or the receiver ID field values.
But you can't actually create such an OR query across multiple fields in Firestore, so that leaves you with a catch-22.
The common workaround is to add an addition array field where you keep the UIDs of all participants in that document:
participants: ["uid1", "uid2"]
Now you can perform a query with an array-contains clause to only request documents that the user is a participant in. Of course you'll also have to modify the security rules to check this new field, rather than the separate sender and receiver fields.

Firestore Security Get with a Where Query using Array Contains produces a permission denied

I've got a Document structure where I have a collection of accounts (_accounts), and each account document in the collection has a subcollection called allowedusers. The documents within allowedusers has a document of each user that has access to the account. Each account document also has field of an array of string of the userids which I'm using to query using Array Contains.
My Firestore rules to ensure that each read is checked against the allowed users is :
match /_accounts/{accountid}{
allow read: if request.auth != null && get(/databases/$(database)/documents/_accounts/$(accountid)/allowedusers/$(request.auth.uid)).data.allowed == true
}
Dart Code from Flutter:
QuerySnapshot querySnapshot = await firestore.collection('_accounts').getAccounts()
.where('userids', arrayContains: _user.id)
.getDocuments();
The above query is producing a Permission Denied :
PlatformException (PlatformException(Error performing getDocuments,
PERMISSION_DENIED: Missing or insufficient permissions., null))
Alternate methods that I've attempted:
Security Rule :
match /_accounts/{accountid}{
allow read: if request.auth != null && resource.data.userids == request.auth.uid
}
Dart Code:
QuerySnapshot querySnapshot = await firestore.collection('_accounts')
.where('userids', arrayContains: _user.id)
.limit(5)
.getDocuments();
For testing purposes I've only got two documents in the _accounts collection, so I'm assuming that the permission denied is coming from hitting the limits when using get within the security rules.
Is there a way of applying security rule and query the collection like this ?
The first security rule is rejecting the query because security rules are not filters. Read more about what that means. It's important to understand this concept, so be sure to read and understand the documentation.
Security rules will only allow a query if it can determine that the query will only find documents that are allowed. It will not check each individual document in the collection for permission and filter out the ones that don't pass the rules. That would not scale at all.
The second rule is rejecting access because it's not correctly checking the array field. If userids is an array field, you can't use an equality expression to compare it with a string as you are with resource.data.userids == request.auth.uid.
If you want to make sure that the user's UID is contained within a document, you will need to treat the field like a list object, and use list operations on it. Use hasAny for that.
allow read: if request.auth != null && resource.data.userids.hasAny([request.auth.uid]);

Securely querying data with firestore - Permission denied

I'm facing a permission denied error when querying firestore, when I have introduced a rule. I have narrowed down my complex rule and filter to the below 2 examples of which one query works, and one doesn't. I do not understand what is wrong with my failing query.
From https://cloud.google.com/firestore/docs/security/rules-query I understand that a rule is not a filter. According to this document: "If a query could potentially return documents that the client does not have permission to read, the entire request fails.".
Baring that in mind, I have been iterating over my rule, filter and data, and come with the below:
The data:
I have NO data in my collection called "MyCollection". As a matter of fact, the collection "MyCollection" has never existed.
The rule:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /MyCollection/{id} {
allow read: if (
(resource.data.readAccess == 0)
)
allow write: if (true)
}
}
}
My failing query (where I have the permission denied error):
Firestore.instance.collection('MyCollection')
.where("readAccess", isLessThanOrEqualTo: 0)
.getDocuments()
.then((_) => print("Success!"));
When I run this query, I get the following error:
W/Firestore(12491): (21.3.0) [Firestore]: Listen for Query(MyCollection where readAccess <= 0) failed: Status{code=PERMISSION_DENIED, description=Missing or insufficient permissions., cause=null}
My successfull query:
(The only difference in this query is that I replaced "isLessThanOrEqualTo" with isEqualTo)
Firestore.instance.collection('MyCollection')
.where("readAccess", isEqualTo: 0)
.getDocuments()
.then((_) => print("Success!"));
Comments:
I have the same results when I do populate MyCollection with data.
It looks like the query is validated against the rule, not the "potential return documents" as the document https://cloud.google.com/firestore/docs/security/rules-query states. If this is the case I wonder how I will be able to translate the following rule into a filter:
(resource.data.readAccess == 0) ||
((request.auth != null) &&
(resource.data.readAccess <= get(/databases/$(database)/documents/App/$(resource.data.appId)).data.group[request.auth.uid])
)
This rule is fairly similar, except that it validates the readAccess level of a document against the group access level in the "App" document for that data's app, for the logged on user. If I can't match the query for a simple rule, I can't imagine what I need to do for this complex rule.
Please advise. Many thanks.
With security rules, the query must exactly match the rules. The behavior you're observe is exactly what I would expect.
With a rule like this:
allow read: if resource.data.readAccess == 0;
That means the query must be filtered exactly like this;
where("readAccess", isEqualTo: 0)
Nothing else will satisfy this rule. It's absolutely demands that the query filter for exactly the value of 0 on the readAccess field. It's not clear to me why you're expecting a different outcome.
Your query suggests that the client provide its own "access" to the collection. Note that this is not secure. You can't depend on client apps self-reporting their own level of access in a database query. Something else on the backend needs to determine if the app is allowed to make the query.
Typically, Firebase Authentication is used to determine who the user is, then allow access based on what that user is allowed to do. You could store the user's permissions somewhere in another document, and use the contents of that document to determine what they can do. Or perhaps use custom claims. But you can't trust the the user pass their own permissions.

Firestore security - Allow read if user's uid equals document's field value uid

I am trying to only allow reads to some documents if the user's uid matches the document's uid.
resource.data.uid == request.auth.uid
The security simulator gives me this errror for this line.
Error: simulator.rules line [20], column [16]. Null value error.
Is this a functionality that is just not supported by Firestore security rules?
If I understood well, I think you want something like this:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow list, create: if request.auth.uid != null;
allow get, update, delete: if request.auth.uid != null && resource.id == request.auth.token.email;
}
}
}
This code should allow users to create their own documents and access/edit only them.
Also, list allowance is required to check if the document exists before creating it, so apparently it needs to be allowed to all authenticated users.
Note that resource.id is actually the document's name. So it means that when you create a document, its name must be the user uid. And of course, you won't be able to have more than one document per collection for the same user.