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.
Related
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
In my Firebase Firestore Rules, I have the following rule for 'courses' collection:
rules_version = '2'
service cloud.firestore {
match /databases/{database}/documents {
match /courses/{courseId} {
allow read: if resource.data.userId == request.auth.uid;
}
}
}
The rules works ok if a read a single document using the firebase web client.
But when querying and the result is empty, for instance the following query:
const results = await app.firestore().collection('courses')
.where('userId', '==', 'unexistingUserId').get();
then the rules fail, throwing an exception on the client side (Missing or insufficient permissions).
Running locally on the emulator, I found that the problem was that the read rule was being validated against a resource containing the query field, even if with the resource not existing:
As shown above, the resource.data.userId contains the "unexistingUserId" value.
To solve this, I had to change the rules by adding validations to check if the resource exists:
rules_version = '2'
service cloud.firestore {
match /databases/{database}/documents {
match /courses/{courseId} {
allow read: if resource==null || resource.data==null || !('id' in resource) || resource.data.userId == request.auth.uid;
}
}
}
Now when querying for no results, the rule validates ok because the 'id' is not present in the request object:
So my questions are:
Is this the expected behavior?
Shouldn't the rules not even being validated if the query returns nothing?
Is there a way to change this behavior to avoid adding those checks in all my rules?
Is this the expected behavior?
Shouldn't the rules not even being validated if the query returns nothing?
This is the expected behavior. What are you are observing is because of the fact that security rules are not filters. Be sure to read and understand that linked documentation.
Queries work differently than single document get(). With a single document get(), the rules system doesn't have to worry about scaling up to match billions of documents, so it's perfectly reasonable to check the contents of the one document to see if it matches. However, since queries can return 0 or more documents, the rules are considered differently. They do not simply check each document that would result from a query. That wouldn't scale at all for very large collections.
Rules work with queries by checking that the filters on the client's query match the constraints in the rule. If you say that resource.data.userId == request.auth.uid, what you are expressing is that users may only ever try to query for documents where userId equals their own UID. They are not allowed to even attempt to match any other document in a query.
The only query that would ever pass this rule would look more like this:
const results = await app.firestore()
.collection('courses')
.where('userId', '==', firebase.auth().currentUser)
.get();
In this case, the filter:
.where('userId', '==', firebase.auth().currentUser)
Exactly matches the rule:
allow read: if resource.data.userId == request.auth.uid;
Any other query would be rejected immediately simply because it's trying to access documents that are known to violate the rule.
I have a where-expression on a collection and listen to updates. I use the exact same expression to grant read access. When the where-condition no longer holds for a document i would expect it to be removed from my snapshot, but an exception is thrown referencing the permission rules.
My rule looks like this
match /arenas/{arenaId}/channels/{channelId} {
allow read : if (request.auth.uid in resource.data.members);
}
My listener is setup like this
ref
.collection("channels")
.where("members", "array-contains", uid)
.onSnapshot(snapshot => {
When i take an id out of members for a specific channel, the where-expression no longer holds for that particular channel. Instead of an update that notifies me that the channel is removed i get an exception.
Is this the expected behaviour, or am i making a mistake?
If it is expected, what would the correct implementation be, or how could i work around it?
This is the expected behavior. If, at any time, conditions change that would result in a security rule rejecting a new query, any existing listeners of that same query will be immediately rejected with an error. A listener can't live beyond its welcome, as far as security rules are concerned.
I can't recommend a "correct" implementation, as you'll have to figure out for yourself what to do if the listener fails. Perhaps you can try the query again after correcting the list of UIDs.
Firestore has a DocumentReference type, which is a "pointer" to another firestore document. Using the firebase JavaScript client, you can access properties (e.g. document "id"), directly on the reference.
For example, if there is a document with a docRef property that is a firestore DocumentReference:
const retrievedDoc = await getFirestoreDocument();
console.log(retrievedDoc.docRef.id); // "jRmSeMYDMKiOPGsmkdaZ"
I am trying to accomplish the same thing within firestore rules. There is a custom function named isOwner. It uses the firestore rules get call on a document path, and then attempts to access the docRef.id just as if it were the JavaScript client above.
get(/databases/$(database)/documents/path/to/$(id)).data.docRef.id
The value of the document's id is compared against the current user's. But when I test this using the simulator and in real code, it fails. I feel like this should work, but it doesn't.
What does work is to store and use the id value directly as a string (e.g. get(/path/id).docId) instead of a DocumentReference.
Should I be able to access the id value of a DocumentReference within the firestore rules? Am I doing something wrong?
I want to avoid doing a second document get within the rule as described in this SO answer. That's a second "read" for each trigger of this rule. And I don't think the document id (which is what I need) will be available on the get call anyway.
Based on documentation:
https://firebase.google.com/docs/reference/rules/rules.firestore#.get
https://firebase.google.com/docs/reference/rules/rules.firestore.Resource
get() method is supposed to returns a Resource object which is supposed to contains a .id property (as well as .data).
For example, to restrict write access to an authenticated user which is the author of a book document (authors documents are identified with the user uid), you would do:
service cloud.firestore {
match /databases/{database}/documents {
match /books/{document=**} {
allow write: if get(resource.data.authorReference).id == request.auth.uid;
}
}
}
Yet I'm always having the error property id is undefined on object on trying.
.data is accessible so I suppose there is an issue in the api.
Update
Actually, a reference is a Path object in Firestore rules as documented here. So you access the id by the index of the part of the path you need.
In this example I use the incoming document's data which has a reference object to lookup a property on another document from a get()
match /databases{database}/documents {
match /contacts/{contact} {
allow create: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.relatedRules[request.resource.data.relation.path[4]].canBeRelated
// the [4] assumes the path to be `databases/$(database)/documents/contacts/contactId`
// your exact index would vary for your data structure
}
}
First Answer
This only works in the Firestore dashboard rules simulator, it is not a working example for either the local emulation or production Firestore.
This is a year old but I encountered this same puzzling issue, but not on the data from a get(), just on the data of the request.resource.data. I'm not sure what ought to be available (not even __name__ is available) in the rules but if you're accessing a resource reference on the data and you have a predictable id size (say, 20 characters) you could simply get the range of the path on the resource to check against
match /databases{database}/documents {
match /contacts/{contact} {
allow create: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.relatedRules[request.resource.data.relation.path[9:29]].canBeRelated
// the [9:29] assumes the path to be `/contacts/20characterLongIdStr`
// your exact range would vary for your data structure
}
}
Feels like a resource reference object should have at least the id since the path is there. It appears Firestore won't support this for whatever reason.
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
}