Let's say I have a collection todos that contains documents that represent todo lists of users.
To secure these documents, often, you can find the following snippets of security rules:
...
match /todos/{todo} {
allow create: if request.auth.uid != null && request.resource.data.ownedBy == request.auth.uid;
allow read, update, delete: if resource.data.ownedBy == request.auth.uid;
}
...
These rules allow CRUD operations on the documents as long as the ownedBy field is the same as the uid of the person performing the requests.
My concern here is that the ownedBy field is also part of that document, meaning that a user can easily modify ownedBy to a different userId. I doubt anyone will do it for any reasons, but from a developer point of view, would that mean it is dangerous to have the field you rely on to be part of the document that can be edited?
Another way to look at it is, this behavior is the same as storing the permissions/authorizations in the same documents. It'd be wrong to store { canEdit: true, canDelete: false} inside that same document, so why is it ok to store the ownedBy field in that document?
What are some good practices to deal with this problem?
"a user can easily modify ownedBy to a different userId"
Given your rules, they actually can't. You're explicitly checking that resource.data.ownedBy == request.auth.uid and request.resource.data.ownedBy == request.auth.uid. Given that request.auth is auto-populated by Firebase and can't be spoofed, the only value they can ever set for ownedBy is their own UID.
I also recommend checking out the Firebase documentation on controlling access per field.
Related
I have an app that user can login to and take and add photos to a feed. im using firestore as the database.
Right now ever user can see every other users photos as well as their own. I want only the user signed in to be able view their own photos.
A user in another forum suggested that I use firestore rules to set that control but I'm having trouble implementing to correct code.
I read through the documentation on firestore and trying to implement the code the way they have it is not working. Below is my database info and my current rules (which is not working) thank you.
Database:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**}{
allow read, write: if request.auth != null && request.auth.uid == 'byId';
}
}
I'm expecting this code to compare the uid of the user the the byId field (which should be the same) and allow them to read and write their own data if true
i also have an authentification page where uid == byId
This check request.auth.uid == 'byId' checks whether the UID of the current user is literally byId. If you want to check whether the UID is the same as ht value of the byId field in the document use:
if request.auth != null && request.auth.uid == request.resource.data.byId;
I also recommend checking out the Firebase documentation on securing document access.
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.
The Firebase Rules docs suggest building conditions comparing the authenticated user's token (i.e., request.auth) with the target Firestore document(s). Something like:
match /posts/{postId} {
allow read, write: if (request.auth.uid != null) &&
(resource.data.tenantId == request.auth.token.tenantId);
}
However, tenantId doesn't appear to be available in Firebase Rules like other related auth fields (e.g., uid, email, email_verified, etc.).
One option appears to be to add tenantId separately as a custom claim using the firebase-admin SDK. But that would create duplicate info on the user object:
{
uid: 'nzjNp3QIfSR6uWy',
emailVerified: true,
displayName: 'pickleR'
...
tenantId: 'wubalubadubdub',
customClaims: { tenantId: 'wubalubadubdub' },
}
An alternative option appears to be to create a tenants collection in Firestore. However, that approach seems to introduce needless complexity and inflate the # of required Firestore queries.
Are there alternatives for accessing the tenantId in Firestore Rules and/or alternative best practices for using Firestore with multi-tenancy?
Having gone down the custom claim route, I've then found the tenant Id is already stored in a nested object as 'request.auth.token.firebase.tenant'
In your example the rule would be:
match /posts/{postId} {
allow read, write: if (request.auth.uid != null) &&
(resource.data.tenantId == request.auth.token.firebase.tenant);
}
The two options you describe are the idiomatic ones:
Pass the information into the rules as part of the ID token as a custom claim.
Look up the information in the database from the rules by the request.auth.uid.
Neither of these is always better than the other: custom claims are more convenient and readable, while using the database is usually faster. It's common to use the database lookup for more volatile information, and claims for information that is "once".
Since this is for a tenant ID, which is unlikely to change quickly, I'd probably go for a custom claim.
I know that Firebase has the FieldValue class, which can be used to generate e.g. a server-side timestamp when writing a document (link).
What's the preferred practice for inserting the current user's uid into a document?
Having the client provide that field seems to allow misuse - unless I provide a server rule that checks for (new/updated) documents to match the request.auth.uid, something like:
service cloud.firestore {
match /databases/{database}/documents {
match /broadcasts/{broadcast}/chatMessagesCollection/{message} {
allow write: if request.resource.data.uid == request.auth.uid;
allow read: if true;
}
}
}
I can't find anything on the web for the use-case of having a document be populated with the user writing it -- so what's the best take on this?
What you're doing now with security rules to enforce that the provided UID matches the current user is exactly the right thing to do. There is really nothing better (for this specific use case), and this is a common practice.
I've even written about it in this blog series: https://medium.com/firebase-developers/patterns-for-security-with-firebase-per-user-permissions-for-cloud-firestore-be67ee8edc4a
The Firestore documentation shows examples of how to secure data using Firestore security rules based on the request.auth.uid field. These typically look something like this:
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{storyid} {
// Only the authenticated user who authored the document can read or write
allow read, write: if request.auth.uid == resource.data.author;
}
}
}
That makes perfect sense.
What I don't understand (and doesn't appear to be shown anywhere) is how to set the resource.data.author field securely.
Obviously that can't just be based from the client because then any authenticated user can tamper with the request to set their author to any value.
I thought maybe we are supposed to use CloudFunctions to set that field but at the moment this doesn't work.
The impact of this is pretty clear in the role based access example:
{
user: "alice",
content: "I think this is a great story!"
}
Surely there must be a tamper-proof way to set the user field there - otherwise any user can make their comments appear to be from anyone else. This seems bad.
In the Firestore example web app, it seems to set the userId field on the client side and I think it is doing the same in the Android version.
What am I missing?
Edit: as #imjared points out this rule implies that 'alice' in user: "alice" is actually a uid, so I think this is safe.
I knew I was missing something.
match /comments/{comment} {
allow read: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter', 'reader']);
allow create: if isOneOfRoles(get(/databases/$(database)/documents/stories/$(story)),
['owner', 'writer', 'commenter'])
&& request.resource.data.user == request.auth.uid;
When the user writes a document to Firebase, they can indeed send any value for the author field they want. But there's no way for them to set request.auth.uid. This last bit in crucial to ensure all (read and write) access is authorized.
The first rules snippet you shared actually has two rules, and it might be easier to separate them out for a moment:
allow read: if request.auth.uid == resource.data.author;
allow write: if request.auth.uid == resource.data.author;
The write rule only allows the operation when the author specific in the request is the same as the request.auth.uid. Since request.auth.uid can't be spoofed, and the value of author will only be accepted if it is the same, the write operation is only allowed if the author field is that of the currently authenticated user.
In fact, that latter rule is more regularly written as:
allow write: if request.auth.uid == request.resource.data.author;
The difference when using request is that it explicitly refers to the document (resource) that is in the write request. The result is the same here whether we use resource or request.resource, but I find it easier to see how security works when thinking of the request here.