I have set up my security rules, and verified that they operate correctly when using get
requests, but fail with list
requests. Below is the basic structure of my database:
/parent_collection
/parent_doc
...data,
[status]: Enum
/sub_collection1
/sub_doc1
...data
/sub_doc2
...data
/sub_collection2
/sub_doc1
...data
/sub_doc2
...data
I have the following rule as one of the match statements for the parent_collection
function isPublished(docId) {
let data = get(/databases/$(database)/documents/parent_collection/$(docId)).data;
return 'status' in data && data.status == "published";
}
match /parent_collection/{docId}/{document=**} {
// Deny all requests to create, update, or delete the doc
allow create, update, delete: if false
// Allow the requestor to read the doc if published
allow read: if isPublished(docId)
}
And finally the three client side queries I have written
// is accepted by firestore rules
const getDocById: GetDocById = (docId) => {
return new Promise((resolve, reject) => {
getDoc(doc(firestore, "parent_collection", docId))
.then((doc) => {
resolve(doc.data());
})
.catch((error) => reject(error));
});
};
// is rejected by firestore rules
const getDocsWithCursor: GetDocsWithCursor = (cursor) => {
const docs: Doc[] = [];
return new Promise((resolve, reject) => {
let q = query(
collection(firestore, "parent_collection"),
where("status", "==", "published"),
orderBy("updated")
);
if (cursor) q = query(q, startAfter(cursor));
getDocs(q)
.then((snapshot) => {
snapshot.forEach((doc) => {
docs.push(doc.data());
});
return resolve([docs, snapshot.docs[snapshot.docs.length - 1]]);
})
.catch((error) => {
return reject(error);
});
});
};
// is rejected by firestore rules
const attachCollectionListener: AttachCollectionListener = (docId, callback) => {
return onSnapshot(
query(
collection(
firestore,
"parent_collection",
docId,
"sub_collection1"
),
orderBy("order")
),
(snapshot) => {
snapshot.docChanges().forEach((change) => {
callback(
change.type,
change.doc.data(),
change.newIndex
);
});
}
);
};
I have spent a long time trying to figure out why the list
query failed and I found out several interesting notes from other questions that explain why it fails.
With all that laid out and explained, I have two problems I am trying to solve.
Firestore security rules are checked when the read operation starts and it do not evaluate against all your actual potential documents. Instead the rule merely checks whether the read operation is certain to only access data that it is allowed to access based on static analysis of the read operation/query against the conditions you set in the rules.
The way you check whether the document is published in your security rules requires that it performs a get
call for each document that it evaluates for a list
call, which isn't possible according to the above. That's essentially require a join on each document, which wouldn't scale if it was possible.
The short summary of this is that Firestore queries can only filter on data that is present in the documents they return.
If you only want to read documents from the parent_collection
you could actually get what you want, as you wouldn't need the get
call in the rules and could simple do:
match /parent_collection/{docId}/{document} {
// Deny all requests to create, update, or delete the doc
allow create, update, delete: if false
// Allow the requestor to read the doc if published
allow read: if 'status' in resource.data && resource.data.status == "published"
}
If you want to allow a read on subcollections under parent_collection
too (as its name implies), then the data you want to filter on will have to be also included in each document in those subcollections. There's no workaround for that in this case.