I'm attempting to setup security rules that allow access to a collection, based on the value of a document field in a subcollection.
This works as expected when retrieving an individual document by id, which is a get operation. However, when querying main_collection (a list operation), this fails with a "permission denied" error. Since there is only a single document in the collection, this is not a case where I don't have permission to some of the documents being queried, such as on this question.
My database structure looks like the following. It contains the collection being listed (main_collection), which has a single document (some_doc), which has a single subcollection (sub_collection), which has a single document (another_doc).
/main_collection/some_doc/sub_collection/another_doc
another_doc has one string field someFieldValue.
For this example, my query is of the entire collection, which is the single document. In my actual application it only queries the documents it expects to have access to, but the end result here is the same because I cannot filter against a document's subcollection from the client library.
firestore.collection('main_collection').get()
These are my security rules.
service cloud.firestore {
match /databases/{database}/documents {
match /main_collection/{mainColDoc} {
// This operation works
allow get: if subCollectionDocumentHasField('someFieldValue');
// This operation fails with permission denied
allow list: if subCollectionDocumentHasField('someFieldValue');
// This checks for the existence of a field on the subcollection's document
function subCollectionDocumentHasField(fieldName) {
return get(/databases/$(database)/documents/main_collection/$(mainColDoc)/sub_collection/another_doc).data.keys().hasAny([fieldName]);
//return get(/databases/$(database)/documents/main_collection/some_doc/sub_collection/another_doc).data.keys().hasAny([fieldName]);
}
}
}
}
The subCollectionDocumentHasField function checks for the existence of the someFieldValue field on the document another_doc. In this function, if I replace the $(mainColDoc) variable with the hard-coded document id some_doc, the list operation is successful. Since the $(database) path variable can be used in this context, I would expect that others could be as well.
Is this a bug or expected behavior?
This is actually the expected behavior, you can't use Firebase's rules to filter the results of your query.
A typical scenario would be to have collection of messages, where each message refers to its creator.
You can't simply add a rule where reading is only allowed on messages for which creator is the authenticated user, to filter automatically the messages of the current authenticated user.
The only way to go is to query with filter on the client side (or through a Cloud function).
The documentation is very clear about this :
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.
From Firebase's documentation
I opened a ticket with Google and confirmed effectively what #José inferred from usage, which is that a security rule "is only checked once per query".
For clarification, while a security rule on a list operation will typically not query the contents of a document (to avoid potenitally-poor performance), there is at least one condition when it will query the contents of a document. This is when the security rule is guaranteed to return only one document. When this guarantee is met, the single document's contents will be queried because high performance can be maintained; the same as on a get operation.
So, in the linked example in my question where the list operation's rule is referencing a parent document, this guarantee is met and the parent document's contents will get queried.
Also, in my example where the list operation's rule is referencing a hard-coded document id, this guarantee is met and the hard-coded document's contents will get queried.
For the sake of stating it explicitly, for a list operation, in any case where Firestore cannot guarantee that its rule will only query a single document, access will be automatically denied, by design.
To reiterate what the other answers say, but stated in a slightly different way: The query must be consistent with the security rules, before any query documents are looked at, or it will fail with permission denied.
For example, if all of the documents in a sub-collection happen to match the security rule (e.g., your create and list rules both require the owner field is "X"), the query still must match the security rules (e.g., the query must also filter on owner is "X") or it will fail with a permission denied error, independent of the actual content of the sub-collection.
Related
Imagine the case I want to request a chat (a document) from two user Ids (user1 and user2).
Chat document contains an array with participants ids so users1 and users2 are both in particiants array.
If I request everything that contains user1 or user2 in participant array I may get user1 chats with other users too, not only user1 with user2.
Code for the example :
ref.where('participantsIds', 'array-contains-any', [u1.id , u2.id])
Now imagine I add a security rule that only allow getting chats if the user making the request is in participants array.
Rule for example :
allow read, write: if request.auth!=null && request.auth.uid in resource.data.participantsId
Is this a bad practice? Can I use rules to control data behaviors for each instance of the app?
The rule will probably add some milliseconds (or not even?) to the request no matter if the result is true or false.
And yes I know will affect cost and request quotas.
Thanks !!
Firebase security rules are not filters. They can't change the data that would be returned from a query. All they do is stop people from accessing data based on the logic you supply.
If you apply the rule shown above, then any query where the user requests any document where their uid is not in participantsId, or they make a query that does not use their uid as a required filter on participantsId, then the query will simply fail. This is something that you should pretty easily be able to observe for yourself.
I suggest reading: What does it mean that Firestore security rules are not filters?
I need a clarification on the infamous "Rules are not filter"
Imagine
a collection testcol with documents that all have a field test with value 1.
a rule for this collection allow list: if resource.data.test == 1
a query that pulls all the documents from this collection: firebase.collection(testcol).get()
So even if the query does not explicitly specify a filter on the field test, all returned documents still pass the rule.
I am testing this now and I see that I am still getting a "Missing or insufficient permissions" error! So is the rule engine not only checking the documents accessed against the rules but also checking the query itself to see if it might theoretically return documents not matching the rules, regardless of the documents themselves?
Firestore doesn't actively check each existing document to see if it matches the conditions. That would never scale.
It instead checks if the read operation is guaranteed to ever only return allowed documents, no matter the actual underlying data. If it can't guarantee that, the read is rejected as it is here.
I have three types of collections in my database.
User
Post
Like
I make a 'like' document such that it has a _user and _post attribute that stores the user and post id respectively.
Currently I am writing a database rule such that a 'like' document is not created if there already exist a document which has the attribute value _user with the user trying to create the object and the _post which has the id the user is liking.
How do I write a rule for this situation ?
I don't think Firestore has any rules that can enforce this at the moment. But I do have some suggestions for alternative approaches:
1. Replicate the like user id to the Post with a cloud function:
You can set up a Firebase Cloud Function that triggers on Create operations of the Like collection. This Cloud Function writes the id of the user into an array on the post called like_users. Then you can have a rule that say:
allow create: if request.auth.uid not in get(/databases/$(database)/documents/Post/$( request.resource.data._post).data.like_users;
(Only problem with this solution, I don't think there is such a thing as "not in", this needs some research)
2. Check if there is a duplicate with a Cloud Function:
You can set up a Firebase Cloud Function that triggers on Create operations of the Like collection. In the function you query for Like documents with the same user and post id. If a document already exists, you delete the most recent one.
3. Restructure your database:
You could remove the Like collection, and instead have an array called "like_users" on the Post collection. This will need you to put a rule that checks if the user exists in the "like_users" array, so I think we might have the same problem as in alternative 1. You will also need to have rules saying that the user is not allowed to update the other fields of the document, which is possible.
When using the Firebase console it is possible to see all documents and collections, even subcollections where the path has "documents" that do not exist.
This is illustrated in the picture included here, and as stated in the docs and on the screenshot as well. These documents won't appear in queries or snapshots. So how does the console find these nested subcollections, when a query does not return them?
Is it possible, somehow, to list these documents. Since the console can do it, it seems there must be a way.
And if it is possible to find these documents, is it possible to create a query that fetches all the documents that are non-existant but limited to those that have a nested subcollection? (Since the set of all non-existant documents would be infinite)
The Admin SDK provides a listDocuments method with this description:
The document references returned may include references to "missing
documents", i.e. document locations that have no document present but
which contain subcollections with documents. Attempting to read such a
document reference (e.g. via .get() or .onSnapshot()) will return a
DocumentSnapshot whose .exists property is false.
Combining this with the example for listing subcollections, you could do something like the following:
// Admin SDK only
let collectionRef = firestore.collection('col');
return collectionRef.listDocuments().then(documentRefs => {
return firestore.getAll(documentRefs);
}).then(documentSnapshots => {
documentSnapshots.forEach(doc => {
if( !doc.exists ) {
console.log(`Found missing document: ${documentSnapshot.id}, getting subcollections`);
doc.getCollections().then(collections => {
collections.forEach(collection => {
console.log('Found subcollection with id:', collection.id);
});
});
}
});
});
Note that the Firebase CLI uses a different approach. Via the REST API, it queries all documents below a given path, without having to know their specific location first. You can see how this works in the recursive delete code here.
Is it possible to create a query that fetches all these subcollections that are nested under a document that does not exist.
Queries in Cloud Firestore are shallow, which means they only get documents from the collection that the query is run against. There is no way in Cloud Firestore to get documents from a top-level collection and other collections or subcollections in a single query. Firestore doesn't support queries across different collections in one go. A single query may only use properties of documents in a single collection or subcollection.
So in your case, even if one document does not exist (does not contain any properties), you can still query a collection that lives beneath it. With other words, you can query the queue subcollection that exist within -LFNX ... 7UjS document but you cannot query all queue subcollection within all documents. You can query only one subcollection at a time.
Edit:
According to your comment:
I want to find collections that are nested under documents that do not exist.
There is no way to find collections because you cannot query across different collections. You can only query against one. The simplest solution I can think of is to check if a document within your items collection doesn't exist (has no properties) and then create a query (items -> documentId -> queue), and check if has any results.
Edit2:
The Firebase Console is telling you through those document ids shown in italics that those documents just does not exist. Those documents do not exist because you didn't create them at all. What you did do, was only to create a subcollection under a document that never existed in the first place. With other words, it merely "reserves" an id for a document in that collection and then creates a subcollection under it. Typically, you should only create subcollections of documents that actually do exist but this is how it looks like when the document doesn't exist.
In Cloud Firestore documents and subcollections don't work like filesystem files and directories you're used. If you create a subcollection under a document, it doesn't implicitly create any parent documents. Subcollections are not tied in any way to a parent document. With other words, there is no physical document at that location but there is other data under the location.
In Firebase console those document ids are diplayed so you can navigate down the tree and get the subcollections and documents that exist beneath it. But in the same time the console is warning you that those document does not exist, by displaying their ids in italics. So you cannot display or use them because of the simple fact that there is no data beneath it. If you want to correct that, you have to write at least a property that can hold a value. In that way, those documents will hold some data so you can do whatever you want.
P.S. In Cloud Firestore, if you delete a document, its subcollections will continue to exist and this is because of the exact same reason I mentioned above.
I have following character collection structure in my database (firestore)
/characters/{uid}
- username: string
- clan: string
- mana: number
- health: number
etc...
I am trying to figure out a security rule for /characters/{uid} with following logic
service cloud.firestore {
match /databases/{database}/documents {
// Characters
match /characters/{characterID} {
allow create: if isValidUsername();
}
}
}
here function isValidUsername checks for various things like length, special characters etc... but one thing I can't figure out is how to check following inside of the function
Make sure that request.resource.data.username is unique i.e. not present inside any other document of /characters collection.
TL;DR: Enforcing uniqueness is only possible by creating an extra collection.
In your current structure, to know if a username is unique, you will need to read each document. This is incredibly inefficient, and on top of that it isn't possible in security rules, since they can only read a few documents per rule.
The trick is to create an extra collection usernames, where you also have a document for each user, but now the key/ID of each document is the username. With such a collection, you can check for the existence of a certain document, which is a primitive operation in the security rules.
Also see:
Prevent duplicate entries in Firestore rules not working