Is it possible to create firestore rules based on specific fields? - firebase

I'm going to try to generalise a bit here to make the question simpler. I have a Firestore database that stores users and forum posts. I'm trying to write the rules for the forum posts specifically.
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null
}
function isOwner(userID) {
return request.auth.uid == userID
}
// ...
match /forum/{topicID} {
allow read: if true
allow create: if isSignedIn()
allow update, delete: if isSignedIn()
}
}
So here, I've got it so that you can create, update and delete if you are signed in. I also need to add the check, "Do they own this post?", with the isOwner() function.
That's all chill, but my problem is - each forum post (AKA topic) has two fields in them that can be updated by anybody who is signed in. These fields are likes and likeCount.
likes is an array of strings, each string is a user's id. likeCount is a number equal to the likes array length.
I'm starting to see that this would have been easier if likes was a sub-collection and likeCount could just be likesSnapshot.docs.length. I just fear the amount of recoding that might involve!
So, long story short, I want to do something along the lines of:
match /forum/{topicID} {
allow read: if true
allow create: if isSignedIn()
allow update, delete: if isSignedIn() && isOwner(resource.data.user.id)
match /likes && /likeCount {
allow create, update: if isSignedIn()
allow delete: if isSignedIn() && isOwner([THE USER ID THAT IS BEING REMOVED FROM THE ARRAY])
}
}
...but I'm not sure of the best way to go about it!
Pleases and thank yous in advance :)

It's not possible to target specific fields like you are showing in security rules. You can only match whole documents with a match statement.
If you want make sure certain users can only modify certain fields, you can use the MapDiff api to check that only those fields are being changed in the document data. It will go something like this:
if isSignedIn() &&
request.resource.data.diff(resource.data).affectedKeys().hasOnly(["likes", "likeCount"])
This will evaluate true if only likes and likesCount are being modified, and the user is signed in.

Yes you can do this. I actually did it recently in my own forum.
The way to do it is to allow update if isOwner and restrict what non owners are able to update. You can do that like this:
allow update: if request.auth.uid != null
&& request.resource.data.{some field} == resource.data.{some field}
&& request.resource.data.title.{some other field} == resource.data.{some other field}
Put all data fields that you don't want non owners to be able to update in the {some field} placeholder. The rule is checking that certain fields have not changed and thus restricting what a user is allowed to change in a document.
Then allow owners full update privs:
allow update: if isOwner()

Related

Firebase security rule allow access only to nested item

I'm following the suggested data model from this tutorial for counting upvotes in a firebase database:
-| upvotes
-| itemId
-| userId: number
This works fine but the problem I'm encountering is how to allow users to only be able to update their own upvote. If I use something like this:
match /votes/{id} {
allow read: if true;
allow write: if request.auth != null;
match /{uid} {
allow read: if true;
allow write: if uid == request.auth.uid;
}
}
Then any authenticated user can edit the entire vote entry. But if I try to restrict to just the /uid match:
match /votes/{id} {
allow read: if true;
match /{uid} {
allow read: if true;
allow write: if uid == request.auth.uid;
}
}
Then I get insufficient permissions when I try to alter the record. Any ideas how to work around this?
In a situation like this where you have mixed private and public access, you might find it easier to break apart the write rule into its components: create, update and delete.
Using the features of the Map, MapDiff and List interfaces in your security rules allows you to implement the following restrictions:
match /votes/{id} {
// anyone can read
allow read: if true;
// Anyone can create a new vote pool, as long as they mark
// themselves the owner first, they can optionally cast their
// own vote at the same time.
allow create: if request.auth.uid == request.resource.data.ownerId
&& request.resource.data.keys().hasOnly(["ownerId", request.auth.uid]);
// Anyone can update the vote pool, but they can only change data
// under their own userId (such as adding/updating/removing their
// vote). Even owners can only update their own vote once created.
allow update: if request.resource.data.diff(resource.data).affectedKeys().hasOnly([request.auth.uid]);;
// only the owner can delete a vote pool
allow delete: if request.auth.uid == resource.data.ownerId;
}
Important: These rules do not currently restrict what is written under the user's ID. With the current rules, a malicious user could write large amounts of data under that key. It is up to you to decide what can and can't be written there.
Note: You may want to adjust the above rules so that you can give the vote pool a title. I'll leave it as an exercise to the reader on how to make sure it is provided at creation with a proper value. You can optionally make the title editable, but that should be discouraged as the results can be viewed differently depending on the title change. You can also implement the ability for the owner to lock the vote pool and prevent further changes.

Firestore security rule to allow content sharing between 2 users?

I need to create a collection where the documents can only be viewed and edited by 2 users who both validated that the other is them partner.
My idea was to have a structure like this :
(Joe & Mary are the users uid)
users:
Joe:
partner: Mary
...
Mary:
partner: Joe
...
partners:
JoeMary:
Joe: true
mary: true
field: value
field: value
...
My first idea was to use the rule:
match /partners/{partnersId} {
allow read, update, delete: if request.auth != null && partnersId.contains(request.auth.uid);
allow create: if request.auth != null;
}
However it undelined with red arrows and someone told me that it is not working that way. No idea why...
Then I had this idea:
const useruid = request.auth.uid
const partner = get(/databases/$(database)/documents/users/$(request.auth.uid)).data.partner
allow read, update, delete: if get(/databases/$(database)/documents/partners/$(useruid+partneruid)).data.$(request.auth.uid) == true;
allow read, update, delete: if get(/databases/$(database)/documents/partners/$(partneruid+useruid)).data.$(request.auth.uid) == true;
This is a pretty complicated rule with 3 reads that have some cost. And of course there is red evrywhere...
Is there a better option for that ?
Thanks
There are many ways to ago about this. It's hard to know what exactly will work for you, since your requirements are a bit vague.
One way is to simply put both of the user's UID strings into a single array field in the document. Then write a rule to make sure the current user's UID is in that array:
match /partners/{partnersId} {
allow read, update, delete: if resource.data.partners.hasAny([request.auth.uid]);
allow create: if request.auth != null;
}
The requirement here is that the document must contain an array field called "partners" with the strings for the collaborating UIDs. The document ID doesn't matter. It could just be a random ID.

Firestore rules based on path component and other collection

I am trying to establish a rules, where user would only be able to perform operations on Chat messages after they have made a purchase of one of the services.
My database structure looks like this:
I have purchases collection: purchases/{purchaseId} which contains buyer_id field.
I also have messages collection: /channels/{purchaseId}/thread/{threadId}.
I want to allow CRUD operations if the users in thread are the same as buyer_id from purchases collection, in purchaseId document.
This is what I've tried, using current user's ID (auth) for now. This doesn't work, either.
Ideally, I would substitute request.auth.uid with the field from the document I am trying to access, but this would do for now.
match /channels/{purchaseId}/thread/{threadId} {
allow read, create, update, delete: if get(/databases/{database}/documents/purchases/{purchaseId}).data.buyer_id == request.auth.uid;
}
I am getting the standard permissions error. What am I missing?
You syntax is wrong when defining the path. Try this:
match /channels/{purchaseId}/thread/{threadId} {
allow read, write: if get(/databases/$(database)/documents/purchases/$(purchaseId)).data.buyer_id == request.auth.uid;
}
Substituting request.auth.uid with the field from the document:
match /channels/{purchaseId}/thread/{threadId} {
allow read, write: if get(/databases/$(database)/documents/purchases/$(purchaseId)).data.buyer_id == resource.data.buyer_id;
}

Cloud Firestore Security Rules - only allow write to specific key in document

I'm currently writing some rules for my app with a Firestore database.
Currently everyone can read data and authenticated users can write.
match /quizzes/{quizId} {
allow read;
allow write: if request.auth != null;
}
That works fine, but I also want unauthenticated users to write only to a specific key in a document.
Example content of a document:
{
title: 'title',
plays: 12,
playedBy: [//Filled with user id's],
...
}
Is there any way that limits unauthenticated users to only have write access to the playedBy array and not the other keys of that document?
Sure thing. But it may become a bit involved if you have a lot of fields.
Let's start with the simplest example. Something like this allows an unauthenticated user to write the playedBy as long as that is the only field in the document:
if request.auth != null || request.resource.data.keys().hasOnly(['playedBy'])
This works if the unauthenticated user is creating a new document, or updating an existing one. But it will stop as soon as the document contains more fields, since request.resource.data contains all fields the document will have after the write succeeds.
So the better alternative is to check that only the playedBy is modified, and that all other fields have the same value as before. The tricky bit there is handling the non-existence of fields, which I typically handle with a few helper functions:
function isUnmodified(key) {
return request.resource.data[key] == resource.data[key]
}
function isNotExisting(key) {
return !(key in request.resource.data) && (!exists(resource) || !(key in resource.data));
}
And then:
if request.auth != null &&
request.resource.data.keys().hasOnly(['title', 'plays', 'playedBy']) &&
isUnmodified('title') &&
isUnmodified('plays')
The exact rule might be a bit off, but I hope this is enough to allow you to complete it yourself.
After the earlier answer (late 2019, I believe), Firebase has brought in Map.diff.
Something like:
match /quizzes/{quizId} {
allow read;
allow write: if request.auth != null ||
request.resource.data.diff(resource.data).affectedKeys() == ["playedBy"].toSet()
}
Tested code where I use it can be seen here.

Firestore Security Rules: Allow User To Create Doc Only If New Doc ID is same as User ID

When users log in for the first time, I need to also call a function that creates a document in my firestore users collection to store their profile data. Using Web SDK.
(I was previously using a new user triggered event with firebase functions, but it was too slow to wait for a cold function to spin up).
Security Rule Requirements
Needs to ensure that the user can only create a document if the document id is the same as their user id (to prevent the user from creating other docs). Needs to ensure that this doc doesn't already exist.
Attempt - Works In Simulator, Not IRL
These tests pass in the simulator, but not IRL.
// Allow users to create a doc if the doc ID == their user id
allow create: if path("/databases/" + database + "/documents/users/" + request.auth.uid) == request.path;
OR
allow create: if /databases/$(database)/documents/users/$(request.auth.uid) == request.resource['__name__']
Have also tried this (again, works in simulator, but not IRL)
match /users/{userId} {
// Allow users to read their own profile
allow create: if request.auth.uid == userId;
}
Update
I recently had to update my rule set because of some changes to the way firestore rules worked, and changes in how the "getAfter" function works. Specifically, I am now able to use request.resource for data comarisons. Anyways, it appears that I can accomplish my goals with simpler rules now so I thought I'd update this answer and share.
Goals
User can create a document, only if the new document ID matches their user ID.
User cannot declare themselves an "admin", block create / update / write requests if "admin" is a field (unless they are already an admin)
service cloud.firestore {
match /databases/{database}/documents {
// Allow users to create a document for themselves in the users collection
match /users/{document=**} {
allow create: if request.resource.id == request.auth.uid &&
!("admin" in request.resource.data);
}
// Allow users to read, write, update documents that have the same ID as their user id
match /users/{userId} {
// Allow users to read their own profile (doc id same as user id)
allow read: if request.auth.uid == userId;
// Allow users to write / update their own profile as long as no "admin"
// field is trying to be added or created - unless they are already an admin
allow write, update: if request.auth.uid == userId &&
(
!("admin" in request.resource.data) ||
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true // allow admin to update their own profile
)
// Allow users to read their own feeds
match /feeds/{document=**} {
allow read: if request.auth.uid == userId;
}
}
}
}
Old Answer
So I figured out how to do this in a workaround way. I also had some additional write / update conditions that prevent the user from changing their permission level. This was for some reason, preventing any "creates" from happening. So I had to mirror the same conditions in create, and the write / update rules. For some reason this was necessary.
This new rule structure accomplishes the following
First Section, for create rule
allows the only authenticated users to create documents only in the "users" collection (during the user setup process, a document is created automatically with the same ID as their user id).
does not allow creation of a document containing the "admin" field, which would suggest they are trying to gain admin access.
it seems that validating the id of the document during creation is not possible, hence additional write / update rules below
Second Section - read, update, write
allows users to read / write / update only documents that have the same ID as their user id (user trying to create a document with an ID other than their user id will fail, also prevents the user from spamming creation of tons of docs by manipulating the client-side JS request.)
does not allow users to write / update their profile to include the "admin" field
Rules
service cloud.firestore {
match /databases/{database}/documents {
// Allow users to create documents in the user's collection
match /users/{document=**} {
allow create: if request.auth.uid != null &&
!("admin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data);
}
// Allow users to read, write, update documents that have the same ID as their user id
match /users/{userId} {
// Allow users to read their own profile (doc id same as user id)
allow read: if request.auth.uid == userId;
// Allow users to write / update their own profile as long as no "admin" field is trying to be added or created
allow write, update: if request.auth.uid == userId &&
!("admin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data);
}
}
}
PS
This was not intuitive at all, so if someone has a better workaround, please post it. Also, I'm really hoping that once firestore 1.0 is out, it will bring with it some huge improvements to rules and rule documentation.
A little bit late, but I manage to tweak one of your possible solutions and make it work:
allow create: if path("/databases/(default)/documents/users/" + request.auth.uid) == request.path;
Just had to replace the database variable with (default). Yes, not fancy...
The solution i came up with. My tests showed it's not possible to create other user-docs than the own uid and it prevents normal users to change any admin state.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).isAdmin == true ||
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
}
function signedIn(){
return request.auth.uid != null;
}
match /users/{user} {
// allow updates to own user-doc
allow read, update, delete: if request.auth.uid == user &&
// allow updates to own user-doc if "isAdmin" field is the same as before the update (in case user was already admin)
(request.resource.data.isAdmin == resource.data.isAdmin ||
// or allow updates if "isAdmin" will be set to false
request.resource.data.isAdmin == false ||
// or allow updates if no "isAdmin" field exists after the update
!("isAdmin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data)
);
// allow creation of user-doc with own uid and no others
allow create: if request.auth.uid == user &&
// if no "isAdmin" field is set
!("isAdmin" in getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data);
// give full access to admins
allow read, write: if isAdmin();
}
}
}

Resources