Firestore security rules based on constraint values - google-cloud-firestore

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.

Related

Does a wildcard have to be used for the document segment in a matching path of a Firestore collection group security rule?

db.collectionGroup('private')
.where('members', 'array-contains', userId)
.get()
.then(...)
This query fetches documents successfully if the relevant security rule is set like:
match /{path=**}/private/{document} {
allow read: if request.auth.uid in resource.data.members;
}
However, the similar rule below prevents the same query unexpectedly.
match /{path=**}/private/allowed {
allow read: if request.auth.uid in resource.data.members;
}
In this database,
private subcollections exist only under documents in the rooms collection.
Every private has only a single document with the ID "allowed".
This means /rooms/xxxxxxxx/private/allowed is the only possible path existing, where xxxxxxxx is an auto-assigned document ID.
Therefore specifying the path as /{path=**}/private/allowed looks correct to me.
In fact, "get" queries work in simulations in the playground, so is it a restriction only for collection group queries, or am I doing anything wrong?
FYI, more detailed database structure is described in another question of mine here.
Yes, it is required.
When you perform a collection group query, it's not possible to call out a specific document id in the query (e.g. "allowed"). The query is explicitly asking to consider all of the documents in all of the subcollections of the given name ("private"). Therefore, the rules must allow for those documents to be considered by adding the trailing wildcard.
You can certainly add a filter to the query if you want to get only certain documents with certain field values, but that filter can't be enforced in the rules.

check for absent field with firestore security rules

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

Firestore security rule to restrict writes to fields for both set and update?

I'm trying to write some Firestore security rules that only allow users to write to certain fields in their documents (e.g. email, gender, preferredName, address).
I wrote the following write rule to restrict access to specific fields:
match /databases/{database}/documents {
match /users/{userId} {
allow read: if userIsAuthenticated()
&& userIsAccessingTheirData(userId);
// Users can always write to specific fields
allow write: if userIsAuthenticated()
&& userIsAccessingTheirData(userId)
&& request.resource.data.keys().hasOnly(["preferredName","gender", "email", "address"]);
The rules works well for when we call userDoc.set in code, but it doesn't work when we call userDoc.update.
Using the Firestore rules emulator, I can see that when we call "set" the request.resource.data.keys() only has the fields that are being passed in the call, but when I call "update" all the fields of the document are in the key collection :-( which makes it impossible to filter.
Is there a way to write a security rule that restricts the fields like above that works for both set and update?
The request.resource variable represents the document as it will exist after the operation succeeds (if it succeeds of course). So request.resource does not just contain the fields that are being updated, but also the other values from the existing document.
It's always been possible to check if a field is being updated by comparing request.resource.data.fieldname with resource.data.fieldname.
But recently a new affectedKeys() function was introduced to security rules that shows just the delta:
// This rule only allows updates where "a" is the only field affected
allow update: if request.resource.data.diff(resource.data).affectedKeys().hasOnly(["a"]);
Also see the release notes for Firebase security rules.
you can access document fields using request.resource.data.{field}
for example if you want to restrict updating dob :
service cloud.firestore {
match /databases/{database}/documents {
// Make sure all cities have a positive population and
// the name is not changed
match /users/{user} {
allow update: if request.resource.data.dob == resource.data.dob;
}
}
}
This means that the document can be updated as long as dob hasn't changed which is what we are trying to achieve.

firestore security rules get subcollection document

I'm facing insufficient permissions for this firestore security check.
service cloud.firestore
{
match /databases/{database}/documents
{
match /events/{eventID}
{
allow read:
if get(/databases/$(database)/documents/events/$(eventID)/authorizations/$(request.auth.uid)).data.EVENT_READ == true;
}
}
}
the get document is hardcoded in the firestore database and the simulator returns true but for the actual query returns insufficient privileges.
I tested and moved the authorizations subcollection to the same level as users collection and it works. Am i missing out anything?
Additional testing: Reading the document directly does not result in insufficient privileges. I'm testing to see if it's an issue with listing but to my knowledge read should cover both get and list in firestore security rules.
Update: Listing seems to be the issue here. I tried to list the entire collection with only one document and it results in the insufficient privileges.
Works:
this.angularFirestore.collection('events').doc(eventID).valueChanges();
Doesn't work (updated):
this.angularFirestore.collection('events', query => query.where('admins', 'array-contains', auth.uid)).valueChanges()
My firestore database:
/events/event1_id
- field 1: some string
- field 2: some string
- admins: array of uid strings
/authorizations/<uid> #uid for $(request.auth.uid)
- EVENT_READ: true
Update 2: Updated the doesn't work query string which I tried out. It is intriguing that if i move the /authorizations sub collection out to be the same level as /events collections, the query will not fail.
Your first query works because it's accessing the events collection with a specific document. That specific document meets the rules criteria, because you've arranged for the get() to allow it.
Your second query doesn't work because it's attempting to get all of the documents in events collection. Your rule does not specifically allow that. It looks like you expect your rule to filter out the events that aren't allowed access based on the contents an unknown number of other documents. You need to be aware that security rules are not filters. Please click through to the documentation and read that section. The client must only request documents that are known to be readable according to rules. It can't depend on rules to filter documents that are not allowed.
If you want to be able to query for all events that the current user has access to, your current database structure will not work. You will need to put all the relevant information in the events collection itself. This means you should consider something like putting the UID of each user that's allowed to read the event in the document itself, then filter on that field. Or have some other collection that you can query in this way.

Is there any function for rules in reading all documents in a collection?

I have some trouble with setting up my rules for a firestore project. I try to learn the database setup but can't find any solution for this. So there's no problems when i try to get a document from my collection "lists". But when i try to get all of the documents in the collection "lists" xcode tells me "Missing or insufficient permissions".
My goal is to have users that are able to create documents in collection "lists" but they can only read the documents in "lists" where they appear in the document array "members".
Right now I can add documents in collection("lists") without any problem but I can't read them. I can only read them one by one from xcode with a specific target.
Any tips or ideas?
service cloud.firestore {
match /databases/{database}/documents {
match /{documentId} {
allow read: if request.auth.uid != null;
}
match /lists/{docId} {
allow write: if request.auth.uid != null;
allow read: if request.auth.uid in resource.data.members
}
}
}
Xcode.
//working
let docRef = db.collection("lists").document("j0hHA5TLPETf6JRMbC1s")
docRef.getDocument { ...
//not working due to permission failed
db.collection("lists").getDocuments() { ...
Your rule doesn't work because it assuming that the rule will filter out all the documents that don't match the rule. This is not how security rules work. Security rules are not filters. From the linked documentation:
When writing queries to retrieve documents, keep in mind that security
rules are not filters—queries are all or nothing. To save you time and
resources, Cloud Firestore evaluates a query against its potential
result set instead of the actual field values for all of your
documents. If a query could potentially return documents that the
client does not have permission to read, the entire request fails.
The client must only request documents that would satisfy the rules. Right now, the query is asking for ALL documents in the lists collection, regardless of whether or not the client has access to them. This is going to fail the security rule, because it's attempting to read documents that it doesn't have access to.
What you need to do instead is make your query only request documents that are readable by the user. This means that you should probably be using an array-contains filter on the client to match only documents that the rule would allow it to read.
Actually, you are on the right path, I think if you change your code like this, it will work.
Instead of this:
match /{documentId} {
allow read: if request.auth.uid != null;
}
Use this:
match /lists {
allow read: if request.auth.uid != null;
}
So when you try to access lists without documentId, it will check the auth.uid.
But when you try to access a document ex. lists/1, it will check whether that user exists in the array.