Firebase security rules not working - Can't figure out why - firebase

is there some one that knows how security rules for firestore works?
I'm trying to do something like this but it doesn't work (I don't get access to data).
match /contents/{contentID} {
allow read: if get(/databases/$(database)/documents/users/$(request.auth.uid)/reserved/permissions).data.contents.hasAny([contentID])
allow create, update, delete : if false
}
It seems the problem is contentID since if I do this
match /contents/{contentID} {
allow read: if get(/databases/$(database)/documents/users/$(request.auth.uid)/reserved/permissions).data.contents.hasAny(["3"])
allow create, update, delete : if false
}
and update the document located in user/reserved/permission adding "3" to contents (that is an array field of the document ) it works. It's like contentID is not converted right.
Can someone explains why?
UPDATE
The client code is just
firestore()
.collection('contents')
.onSnapshot((querySnapshot) => {
console.log(querySnapsho)
})
and it return always null. If I change the rule in
match /contents/{contentID} {
allow read: true
allow create, update, delete : if false
}
it works. So the problem is with the rule

The problem is that security rules are not filters. I strongly suggest reading that documentation to understand how the system work.
Your query attempts to get all documents in the collection. The rules deny that query because it's not certain if the client actually has read access to each any every document. It will not evaluate a get() for each possible document - that simply doesn't scale (and it would be very expensive for you for large collections).
Your client app should be able to get() any individual document where the rule evaluates to true, but you won't be able to perform queries against the collection like this.

The rule is well written. The problem is that the generic query get() on the entire collection "contents" it's not allowed after this kind of rules are activated (and it makes sense since this behaviour is designed to reduce the resources needed for a query)
Read this to understand the logic
https://medium.com/firebase-developers/what-does-it-mean-that-firestore-security-rules-are-not-filters-68ec14f3d003

Related

Firestore per-field security rule

I have studied the answer to this question (which has an extremely similar title): Per field rules in Firestore Security Rules. The solution in that case was to make a field unmodifiable, I do not think that is what I am after here.
I have a posts collection with a data structure as follows.
{
uid: string, // this is the original poster's UID
title: string,
content: string,
likesCount: number,
likerUIDs: string[]
}
I would like to restrict writes to the title and content fields to users with an auth token UID that matches the post's uid field. However, any authenticated user should be able to increment the likesCount and add their own UID to the likerUIDs field.
It seems like per-field security rules are not really supported. Is the solution here to maintain a separate collection with different rules but the same keys as the posts, for example post-likes, that contains the likesCount and likerUIDs fields? Or is there a firestore security rule trick to achieving this?
EDIT
Thanks to Doug and Frank's comments (extremely helpful video series by the way), I was able to come up with a solution to my initial question. As suggested in the accepted answer, I'm going to do this with a callable function, since it is perfect for this case. For those who stumble upon this question and want to accomplish something similar, I've pasted the rules I ended up with here. These rules accomplish exactly what is described in the question, but a callable function was definitely the way to go here.
function isOwnerCurrent() {
return request.auth.uid == resource.data.uid;
}
function isOwnerIncoming() {
return request.auth.uid == request.resource.data.uid;
}
function isUnmodified(key) {
return request.resource.data[key] == resource.data[key]
}
match /posts/{post} {
function validateNonOwnerPostUpdate() {
return isUnmodified('title') && isUnmodified('content') &&
isUnmodified('created') && isUnmodified('updated');
}
allow read: if true;
allow create: if isOwnerIncoming();
allow update: if (isOwnerCurrent() || validateNonOwnerPostUpdate()) && isUnmodified('uid');
allow delete: if isOwnerCurrent();
}
For updates, I am checking if the user is either the owner of the post, or only updating the so-called "public" fields of likesCount and likerUIDs, and for both they must not be modifying the owner UID of the post. Like mentioned in the accepted answer, this isn't great because anyone will be able to edit these fields and mess up the numbers.
I think it is better to use cloud function to solve this. you can use callable cloud function when that other users (not document owner) like that post. https://firebase.google.com/docs/functions/callable . because cloud function can bypass security rules
I think it is safer you do it through cloud function unless that likesCount is not that important. because if someone can hack your client app than they can modify your code. you will update the document like this
db.doc(`posts/${postID}`).update({
likesCount: admin.firestore.FieldValue.increment(1)
})
if they hack your app, then they can change the increment from 1 to 100 for example. yes you can do the same via security rules but you have to add additional check , and it will be complicated and error prone IMO.

Firebase collectionGroup security rules match on document key

I have an app using Cloud Firestore. I'm trying to secure my database with Firebase security rules and have been struggling with receiving a document that I'm querying through a collection group query.
Here is my security rule that is passing the emulator, but not inside my web app.
match /{path=**}/groups/{groupId} {
allow read: if resource.data.id == resource.id;
}
If I hardcode my rule to this:
match /{path=**}/groups/{groupId} {
allow read: if resource.data.id == "1" <--- hard coding the value to match my DB, this works;
}
This is how I query for the group:
this.db
.collectionGroup('groups')
.where('id', '==', id)
.get()
.then(snapshot => { ... });
Screenshot:
I wouldn't expect that first rule to work in any situation because it's attempting to filter documents based on their contents. Security rules are not filters. If it works in the console simulator, that might be a bug in the simulator. Also, bear in mind that the simulator does not simulate queries, it just tests document gets.
The second rule works because the client query exactly matches the rules. They are both requiring a document id property of "1". Since the client is specifying the filter, and the filter matches the rule, it's OK.
It's not entirely clear to me what your first rule is supposed to be allowing or rejecting. It looks like you want it to only allow documents whose ID property is the same as its actual document ID. But since the client is not actually capable of expressing that filter condition, the rule is simply rejecting the query every time.

Firebase database Rules o how to filter data

the goal I would to achieve is to show to an authenticated user a list of "Events" shared with other users, and in particular, only the events where the user itself is member of.
the initial structure I had in mind was:
events:{
"eventId1":{
"title":"some text",
"members":{
"myAuthUID":true,
"anotheUserUId":true
}
}
"eventId2":{
"title":"some text",
"members":{
"anotheUserUId":true
}
}
}
the rules I put in place is the following one. but it failed the porpouse:
{
"rules": {
".read":"auth.uid !== null",
".write": "auth.uid !== null",
"events":{
"$evtId":{
".read":"data.child('members').child(auth.uid).exists()",
".write": "data.child('admins').hasChild(auth.uid)",
}
}
}
}
calling /events all the events are showm
calling /events/eventId2 with myAuthUID it succeed.
the result must be, for sure, to show only the eventId1 to muAuthUser
could you please help me to structure better the data model and/or refund the rule?
meanwhile I'll look for a solution to this simple problem :)
thanks
First things first: that top-level ".read":"auth.uid !== null" means that all authenticated users can read the entire database. Once you've granted a permisson on a certain level in the database, you can't take that permission away at a lower level. So your .read rule on /events/$evtId is useless at the moment.
The next step is that rules are not filters. Firebase checks your rules when you attach the listener, and the entire condition will be evaluated at that time (and only at that time). In practice this means that security rules cannot be used to filter data. This has been asked frequently before, so I recommend you check out some of those previous questions on the topic.
Recently Firebase added the ability to secure the queries that you allow on a certain node. So you could allow reads from /users, but then only if they order/filter on a certain property. For more on this, see the documentation on query based rules. However, I'm not sure this will allow your use-case. There was a similar question moments ago, so I recommend monitoring that one too, in case someone answers there: How to filter by orderByChild containing a string in Firebase query-based rules
My usual solution for your use-case: your current data model allows you to efficiently read/query the members for an event. It does not allow you to efficiently read the events for a member. To allow the latter, add an additional data structure:
members
memberId1:
events
eventId1: true
eventId2: true
memberId2:
events
eventId1: true
eventId3: true
Now this is a lot easier to secure.

Allowing access based on mapped key

It's a simple and common use case of security rule, but cannot make it work.
I have a document orgs/fooOrg on my Firestore(not RTDB), and it contains an object
{
"members": {
"fooUser": true
}
}
and the rule applied is
service cloud.firestore {
match /databases/{database}/documents {
match /orgs/{orgId} {
allow read: if "fooUser" in resource.data.members;
}
}
}
I expect all the document in orgs collection should be able to be read; however, the server says Error: Missing or insufficient permissions as a result of running
firebase.firestore().doc('orgs/fooOrg').get()
on a browser (using v4.5.0 and v4.5.1). Even
allow read: if resource.data.members["fooUser"] == true;
fails, too. What went wrong?
In my understanding, this should work according to this document
https://firebase.google.com/docs/firestore/security/secure-data#evaluating_documents_currently_in_the_database
I believe that it was working like a week ago. All the sudden, my working code started to generate the error, so I wrote this MCVE and tested on several different projects.
In addition, I found similar issues below, but a bit different from them, so not sure if it's the same reason (a bug on Firestore)
Firestore security rules based on map values
(My case, even getting a simple document fails)
Firestore read rules with self condition
(This case uses a value of a map. My case, a key is used)
Now seems that the issue is solved without changing code. No announcement, but seems that something is fixed by Firestore side.

Firestore security rules based on map values

I want to store if a user is permitted to read a document in the document itself, based on the user's email address. Multiple users should have access to the same document.
According to the documentation Firestore does not allow querying array members. That'S why I'm storing the users email addresses in a String-Bool Map with the email address as a key.
For the following example I'm not using emails as map keys, because it already doesn't work with basic strings.
The database structure looks like that:
lists
list_1
id: String
name: String
owner: E-Mail
type: String
shared:
test: true
All security rules are listed here:
service cloud.firestore {
match /databases/{database}/documents {
match /lists/{listId=**} {
allow read: if resource.data.shared.test == true
}
}
}
Edit: It also doesn't work if I use match /lists/{listId} instead of match /lists/{listId=**}
How I understand it, this security rules should allow reading access to everyone if the value in the map shared[test] is true.
For completness sake: This is the query I'm using (Kotlin on Android):
collection.whereEqualTo("shared.test", true).get()
.addOnCompleteListener(activity, { task ->
if (task.isSuccessful) {
Log.i("FIRESTORE", "Query was successful")
} else {
Log.e("FIRESTORE", "Failed to query existing from Firestore. Error ${task.exception}")
}
})
I'm guessing that I cannot access map values from the security rules. So what would be an alternative solution to my problem?
In the Firestore rules reference it's written that maps can be accessed like that resource.data.property == 'property' so, what am I doing wrong?
Edit: This issue should be fixed now. If you're still seeing it (and are sure it's a bug with the rules evaluator), let me know in the comments.
I've chatted with some folks here about the problem you're encountering, and it appears to be an issue with the security rules itself. Essentially, the problem seems to be specific to evaluating nested fields in queries, like what you're doing.
So, basically, what you're doing should work fine, and you'll need to wait for an update from the Firestore team to make this query work. I'll try to remember to update this answer when that happens. Sorry 'bout that!
Whenever you have (optional) nested properties you should make sure the property exists before continuing to check its' value eg.
allow read: if role in request.auth.token && request.auth.token[role] == true
in your case:
allow read: if test in resource.data.shared && resource.data.shared.test == true
, I was struggling a long time with roles until I realized that on non-admin users the admin field is undefined and firestore rules just crashes and doesn't continue checking other possible matches.
For a user without token.admin, this will always crash no matter if you have other matches that are true eg:
function userHasRole(role) {
return isSignedIn() && request.auth.token[role] == true
}

Resources