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.
- As it is implemented currently, the wildcard variable has an undefined value for list (query) type requests (but not for get type requests, because the document ID is of course coming directly from the client). Queries might match multiple documents, and your query doesn't specify a document ID, so it can't be used in the rule. (original question).
- A security rule "is only checked once per query", and not on a per document basis. (original question)
With all that laid out and explained, I have two problems I am trying to solve.
- Is it possible to use a Firestore Rule match statement that relies on a wildcard variable for a list query. If not, I can reject all list requests and move them to a Firebase Functions call and use the admin api. While not ideal, migrating to node functions would not bar a major rehaul.
- Is there a way I can structure the rules to support a snapshot listener for a list query. This second problem appears to be the bigger fish to fry as I can't migrate a listener to Firebase Functions (explained here). I use snapshot listeners to support live updates and state syncing between the local and server databases. If I couldn't use listeners a lot of functionality would get removed.