Firestore - disallow reading multiple documents at once - firebase

I've got a Firestore collection.
The IDs of the documents are secrets. You should be able to read only the document whose ID you know.
For the sake of simplicity. I'd like to stick to this approach.
However, by default, one can read an entire collection from Firestore, for example
await firestore.collection("secret_documents").get()
Is it possible to allow reading only one document at once, only when it's referred by its ID?

Yes, that is actually quite easy. To control what documents can be accessed, use Firebase security rules for Firestore.
By default your security rules will be read and write, but those can actually be broken down into more granular operations of get, list, create and update. And what you're trying to do is to allow get, but not a list operation. From the documentation:
service cloud.firestore {
match /databases/{database}/documents {
// A read rule can be divided into get and list rules
match /cities/{city} {
// Applies to single document read requests
allow get: if <condition>;
// Applies to queries and collection read requests
allow list: if <condition>;
}
...
So to allow get for everyone and disallow list calls:
allow get: if true;
allow list: if false;
You'll probably want to elaborate on the allow get rule a bit, because it's more common to restrict it, for example to users that are signed in to your project with Firebase Authentication:
allow get: if request.auth.uid != null;
https://firebase.google.com/docs/firestore/security/rules-query

Related

What security rules should be applied to reads in Firebase?

I created a website for a theatre company with a list of their upcoming shows.
Thes dates are stored in firestore, so I put up some rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read: if true;
}
match /{document=**} {
allow write: if request.auth != null;
}
}
}
I just want the admin to be able to write when they are logged in, but I want everybody to be able to access the database to read the dates.
I thaught it was ok, but I get a new email every day from firebase saying that the database is not secure because anybody can read my data.
What should I do?
Your security rules should allow exactly what your code requires and nothing more. This is known as the principle of least privilege, and is the best way to ensure malicious users can do no more than your own code already does.
Let's look at making reads and writes more secure in turn.
Securing reads
Your rules currently allow anyone to read the entire database. But your code doesn't read the entire database. It instead only reads a list of upcoming shows. So you should only allow reading of upcoming shows:
match /databases/{database}/documents {
match /upcoming_shows/{document} {
allow read: if true;
}
So now users can only read from a single collection: the one named upcoming_shows.
If you actually have a list of all shows, and your code only reads the upcoming shows by using a query, you could also secure that query so that someone reading all shows gets rejected.
As said at the start: your rules should only allow exactly what your code requires, and nothing more.
Securing writes
You said that only the administrator should be allowed to write data when they are logged in. But right now anybody who signs in to Firebase Authentication can write whatever they want in your entire database. So a malicious user can take the configuration from your application, call Firebase with that to sign in, and then for example delete all your data, add their own fake shows to it, or just create an entirely different data set in the database, that you then pay for.
There are two parts that you'll want to better secure:
Only the administrator can write.
They can only add shows.
Only the administrator can write
You know the administrator it seems, so you can probably find their UID in the Firebase console and simply hard-code that in your rules:
allow write: if request.auth == "uid of your known administrator";
Now with these rules, since Firebase determines the UID of the user and it can't be spoofed, you've ensured that only the one person that you identified can write to the database.
There are many variations of this pattern, but this is a good first step.
They can only add shows
With the above changes we already ensured that only the administrator can write, but they can still write whatever they want. The principle of least privilege dictates that we should ensure they can also only write what they must be able to write. In your case that is "adding new shows".
This again breaks down into two requirements:
The administrator can only write shows, meaning they can't write other types of data, or write data elsewhere in the database.
The administrator can only add shows, meaning they can't update or delete them.
Only allow writing of shows
The first requirement is two-fold once more, the first one being similar to what we did for reads: we want to ensure they can only write to the upcoming_shows collection:
match /upcoming_shows/{document} {
allow write: if request.auth == "uid of your known administrator";
}
The second part is that they can only write shows there, meaning you'll want to validate the data that they write. You'll want to only allow the fields that your code actually writes as (again) the rules should only allow exactly what the code does, and nothing more. This could include validating that the date of the show is in the future, if that is also something that your use-case requires.
Only allow adding shows, not updating/deleting them
Then finally your use-case says they can only add shows, which I read as not updating and/or deleting them. We can use granular rules to implement that requirement:
match /upcoming_shows/{document} {
allow create: if request.auth == "uid of your known administrator" &&
/* the validation rules from the previous step */;
}

firebase firestore subcollection access rules

I have a firestore with a collection called "children" and a subcollection called events. The children documents have an array called "caretakers" which contains the authids for users that should have access to this document. My question is, what is the right way to secure the subcollection. I am currently doing the following:
match /children/{childId} {
allow read, write, delete, list:
if request.auth.uid in resource.data.caretakers;
allow create:
if true;
}
match /children/{childId}/events/{eventId} {
allow read,write,delete,get:
if request.auth.uid in get(/databases/$(database)/documents/children/$(childId)).data.caretakers
}
Something about that get(...) doesn't feel right to me. Is that necessary? Do I really need to specify rules separately for each subcollection? or if the parent document has permissions.. those permissions should cascade down to subcollections?
With your current structure unfortunately you will indeed need to read the parent document to check against its caretakers role for each subdocument. What's even worse is that this makes queries impossible, as you can't read from the parent document when querying events.
The common workaround for this is to duplicate the caretakers into each events document, so that you can query for it there, and the rules can then secure that only that query is allowed.
Yes you need to explicitly define rules for sub-collections. You can nest the sub-collection's rule in that collection itself to structure it.
Security rules apply only at the matched path, so the access controls defined on the [children] collection do not apply to the [events] subcollection
service cloud.firestore {
match /databases/{database}/documents {
match /children/{childId} {
allow read, write, delete, list: if request.auth.uid in resource.data.caretakers;
allow create: if true;
// These rule will apply for docs in children collection only
// Explicitly define rules for the 'events' subcollection
match /events/{eventId} {
allow read,write,delete,get: if request.auth.uid in get(/databases/$(database)/documents/children/$(childId)).data.caretakers
// This rule will apply for docs in events sub-collections only
}
}
}
}
You can read more about this at: How security rules work?
I'm answering my own question to note what I ended up going with as a solution.
In my case, the number of Users that have access to each {childId} is very low. Therefore, I ended up storing custom claims in the Users Auth object. Basically stored an array of {childId} in the Users auth object instead of storing a bunch of userIds into the Child object.
This allows me to do a direct check to see if the {childId} exists in the users auth object and saves me extra reads of data.

Why isn't my Firestore Security rule cascading downward?

I can't figure out how to write a Firestore rule that reflects: "when the user requests a doc from the messages collection, check that the doc above that "messages" collection includes a field thats an array which includes their uid. I've gotten the rule to work on the top level but if I try to access a document inside a collection, which is inside the "chats" collection, the user is denied.
Here's my relevant rule:
match /chats/{chatId}/{allChildren=**} {
allow read, update: if request.auth.uid in resource.data.usersUids;
}
This works (the chatId doc has an array of uids):
await db.collection("chats").doc(chatId).get()
This does not work (none of the messages have an array of uids):
await db.collection("chats").doc(chatId).collection("messages").get()
It seems like the heart of the issue is that I'm trying to get the resource.data of the messages collection rather than the resource of the chatId doc, but I can't figure out to solve this.
resource only contains the data from the specific document that was matched by the entire path. It doesn't contain anything from any parent documents that appear in that path.
Any time you want to use fields from a document that wasn't matched by the full path, you have to get() the document using its own full path as described in the documentation. For example:
match /chats/{chatId}/{allChildren=**} {
allow read, update: if request.auth.uid in get(/databases/$(database)/documents/chats/$(chatId)).data.usersUids;
}

Firestore rules allow specific document but not collection

I need to allow a user to read a specific document /customers/cumstomerkey but not to get the whole collection. How can I achieve that? Following rule allows access to get the whole collection:
match /customers/{document=**} {
allow read, write: if true;
}
You will want to read about granular operations in security rules. If you have a single document that should be gettable, just call out the path and allow get access on just that:
match /customers/cumstomerkey {
allow get;
}

Do I need to do a get for every field of a document I want to access

I see that to get a field of a document in security rules one must use get. The example below shows getting the 'admin' field of some document in the users collection. If I wanted to get another field, would I have to do another get request or can I just do one get request and get all the fields I need in the document.
Here is the example I'm referring to in the documentation.
https://firebase.google.com/docs/firestore/security/rules-conditions
service cloud.firestore {
match /databases/{database}/documents {
match /cities/{city} {
// Make sure a 'users' document exists for the requesting user before
// allowing any writes to the 'cities' collection
allow create: if exists(/databases/$(database)/documents/users/$(request.auth.uid))
// Allow the user to delete cities if their user document has the
// 'admin' field set to 'true'
allow delete: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true
}
}
}
Yes, you would have to write another get(). There are no variables in Firestore security rules, so you can't store the contents of a get() in order to use its data multiple times.
Multiple gets accessing the same document might not incur multiple read charges. The documentation states:
Some document access calls may be cached, and cached calls do not count towards the limits.

Resources