Firestore rules: Prevent overwrite of property - firebase

In Firestor rules, how can I allow new documents to be created to a collection, and new values to be added to a document but not values in the document to be overwritten?
Please see the following for a clearer understanding:
Collections -------------- Documents -------------- Values
Users -------------------- DonutCoder -------------- isAdmin, birthDate
Now, if I want to add email to the values under the document Donut Coder how can I do this but prevent overwriting (eg. changing isAdmin to true so the user gets more priveleges)
What I have tried: allow read, create - but this allows new documents to be created but no properties in the document to be changed.

1) Don't add any code that overwrites the same path.
2) TO prevent others from reverse-engineering and update it, add this:
allow update: if false
Again anyone can still attempt to create the same document again so you need to add more security rules to it. Do check the official documentation for that.
I have answers regarding similar questions but they refer to realtime database. Though you can check it here.
The allow update: if false just prevent updating the existing value.

This official documentation page seems to show how to do this: https://firebase.google.com/docs/firestore/security/rules-fields#preventing_some_fields_from_being_changed
allow update: if (!request.resource.data.diff(resource.data).affectedKeys().hasAny(['average_score', 'rating_count']));

Preventing isAdmin or birthDate being updated:
allow update: if request.resource.data.isAdmin = resource.data.isAdmin
&& request.resource.data.birthDate = resource.data.birthDate;
You can start building on this depending on your needs. For instance, if you want to allow only logged in user and modifying only one field at a time:
allow update: if request.auth.uid != null
&& request.resource.data.diff(resource.data).affectedKeys().size() == 1
&& request.resource.data.isAdmin = resource.data.isAdmin
&& request.resource.data.birthDate = resource.data.birthDate;

Related

Firestore and Rules | Allowing/disallowing read access to post under a public/private user?

Situation
I have the following Firestore setup
/posts/{id}
/posts/{id}/comments/{id}
/users/{id}/followers/{userId}
A user profile can either be public or private. All users can see posts by public users, but only users who follow private users can see said post, ie. they are in the owner's followers collection.
Current Solution
The post doc looks like this:
owner_account_visibility: public || private
ownerId: uid
The comment doc looks the same:
owner_account_visibility: public || private
ownerId: uid
My rules look like this
match /events/{eventId} {
allow read: isValid();
match /eventComments/{commentId} {
allow read: isValid();
}
}
function isValid(){
return (resource.data.owner_account_visibility == "public" || exists(/users/$(resource.data.ownerId)/followers/request.auth.uid)))
}
Problem
I see problems/questions with this solution:
Problem: A user may create many posts, which in turn may have lots of comments. This means that if a user updates their account visibility, a cloud function has to update possibly thousands of post and comment documents
Problem: A user may load many private posts and comments, and for each one of those is a database read, which can get very expensive as the user scrolls their feed
Question: In the isValid() function, there are two conditions seperated by an OR sign (||). Does this mean that if the first condition returns true (resource.data.owner_account_visibility == "public") then the function will not check the second condition (exists(/users/$(resource.data.ownerId)/followers/request.auth.uid)), saving me a database read? If this isn't the case, then I will waste a loooot of reads when a user loads tons of comments from a post even though it is public...
Does anyone have a proposed solution to this problem? Any help would be appreciated :)
I solved this myself. In short, instead of letting a user set their accounts' visibility, I let them set each post's visibility. This is simply because that is the functionality I want in my app. Now, I can simply use resource.data.post_visibility == "public", avoiding the issue of having to update every post if a user changes their account's visibility. If the first condition is false, I do as I did in my current solution in the question (exists(/users/$(resource.data.ownerId)/followers/request.auth.uid)). Also, comments and replies to a post are opened to all authenticated users even though the post is set to private, since comments aren't necessarily the post owner's own content/sensible information

Firestore Security: How to create rules based on a changed property which identifiers a document

Imagine a User Document which holds a property "restaurants" witch again holds keys for all restaurant he is responsible for.
I want to implement a security rule which only allows updating the property "restaurants" if the changed restaurant-id in the update, references a restaurant owned by the user.
Therefore in security rules i want to detect the changed-restaurant-id, load corresponding restaurant-doc and check if the field "owner" inside the restaurant-doc is equal the user-id.
I was already able to implement a rule to check only 1 Id was modified by implmenting something like this:
function hasSingleChange(){
let changedKeys = request.resource.data.diff(resource.data).affectedKeys()
return changedKeys.size() == 0;
}
Now I wanted to get the the specific Id that changed and use it for building the query for the restaurant:
function getChangedKeys() {
let changedKeys = request.resource.data.diff(resource.data).affectedKeys()
return changedKeys[0]
}
But unfortunately "return changedKeys[0]" will not work as affectedKeys returns a Set with limited operations available.
Is there an other way around to load a document in security-rules, based on changed property to be able to apply checks on this changed, referenced document?
Edit
Let me explain you what i actually want to solve with this.
I have a Domain consisting of graphs, nodes and users as shown here:
What I want to accomplish is that users can access read/update/delete a node if the node is in a at least one graph which the user is the owner for. In the example above the user should have acess to node "n1" but not to node "n2".
My idea was to check the permission on the node as following:
match /nodes/{nodeId} {
allow read: if resource.data.graphs.keys().hasAny(get(/databases/$(database)/documents/users/$(request.auth.uid)).data.graphs.keys());
I guess this would work (?). But I need somehow to protect the user-Document to allow only writes to the "graphs" field if the user is listed as owner in the graph.
Thats why I want to add another security-rule, something like this:
match /users/{userId} {
allow update: if request.auth.uid == userId && hasSingleChange() && get(/databases/$(database)/documents/graphs/getChangedKey()).data.owner).keys().hasAny($(userId))
}
function hasSingleChange(){
let changedKeys = request.resource.data.diff(resource.data).affectedKeys()
return changedKeys.size() == 0;
}
//To get the id of the changed graph
function getChangedKey() {
let changedKeys = request.resource.data.diff(resource.data).affectedKeys()
return changedKeys[0];
}
But as mentioned this is not working as I am unable to extract the changed graph-Id to lookup the document (getChangedKey not working).
Maybe there is another way arround to fullfill my requirements? What I try to avoid is to entitle the userId directly on the node, as I would have to update a lot of nodes if someone entitles a new user to a specific graph.
As you want an end-user to be able to modify only a document's field. You can check the section “Preventing some fields from being changed
” by referencing the documentation. You can try with the permission below.
allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
.hasAny(['A', 'B']));
You check the discussion on stackoverflow link & Stackoverflow url.

Firebase how check path contains value and allow read and write other path

At the moment i try to build a chat in flutter with google firebase.
Now i would like my database more secure.
That means only users (room_3) in chat can read and write data data.
Is it possible to check a path contains a user value
and if the user value is contains allow read a other path?
Here my database structer:
/chat/product_id/product_id_12345/chat_room_id/room_1/message_1/message2...
My idea is i add in 'room_1' the user id.
Then i check the user is contains in 'room_1'.
If the user is contains i allow read and write data the complete path (message_1/message_2...).
Here my example:
If you have any questions fell free to ask me.
Many thx.
In the security rules for the messages subcollection you can read the parent document and check whether the current user is in the user_id field with:
...
match /messages/{message} {
allow read:
if request.auth != null &&
request.auth.uid in
get(/databases/$(database)/documents/chat_room/$(chat_room_id)).data.user_id
}

Firestore security rule that only allows empty documents

I'm basically trying to use a firestore collection as a an email list. Anyone can create a document that has their email as the id and nothing more. The tricky part is the "and nothing more" bit. When no data is provided in the request, request.resource is undefined which you can't check for in security rules to my knowledge. Is this possible? Or is it necessary to have something like one mandatory field for this use case?
Having empty documents regularly leads to issues down the line. Why not require a single marker field, and validate that in rules?
request.resource.data.keys.hasOnly("marker")
For the benefit of others looking to make an email list in firestore, this is the full rule I ended up using:
match /email-list/{email} {
allow get: if true;
allow list: if false;
allow create: if request.resource.data.keys().hasOnly(["marker"])
&& request.resource.data.marker == true
}

Request.auth.metadata in security rules?

I have a Firebase project where I'd like for users to be able to see when other users created their profiles. My initial hope was that I could use "user.metadata.creationTime" on the frontend to pass the date into the user's extra info document and verify that it is correct by having "request.resource.data.datecreated == request.auth.metadata.creationTime" as a Database Rule, but it looks like it is not possible according to the documentation.
Is there any way I can verify that the creation date is correct on the backend?
More info edit: Below is the code that is being triggered when a user creates a new account on my profile. The three values are displayed publicly. I'm creating a niche gear for sale page so being able to see when a user first created their account could be helpful when deciding if a seller is sketchy. I don't want someone to be able to make it seem like they have been around for longer than they have been.
db.collection('users').doc(user.uid).set({
username: "Username-156135",
bio: "Add a bio",
created: user.metadata.creationTime
});
Firestore rules:
match /users/{id} {
allow get;
allow create, update: if request.resource.data.username is string &&
request.resource.data.bio is string &&
request.resource.data.created == request.auth.metadata.creationTime;
}
user.metadata.creationTime, according to the API documentation is a string with no documented format. I suggest not using it. In fact, what you're trying to do seems impossible since that value isn't available in the API documentation for request.auth.
What I suggest you do instead is use a Firebase Auth onCreate trigger with Cloud Functions to automatically create that document with the current time as a proper timestamp. Then, in security rules, I wouldn't even give the user the ability to change that field, so you can be sure it was only ever set accurately by the trigger. You might be interested in this solution overall.

Resources