Firebase security rules resource.data not working - firebase

The goal is essentially to compare an element of something, such as a Last in Messaging, and just to check it isn't null or equal to another value.
match /Messaging/ {userId} {
allow read: if resource.data.Last != null;
}
resource.data is not null, but getting any field from the firestore database (Last in this case) is null.

Related

Firebase query returns "PERMISSION_DENIED" but rules work in test console

I have the following Firebase rule:
match /PendingInvites/{inviteID} {
allow read: if request.auth != null &&
isInviteForUser(database, inviteID);
}
and the following functions:
function isInviteForUser(database, inviteID) {
let dataItem = get(/databases/$(database)/documents/PendingInvites/$(inviteID)).data;
return (dataItem.userPhone == request.auth.token.phone_number) ||
(dataItem.userEmail == request.auth.token.email);
}
The testing against the collection using the online role test works, I've verifies using real documents and with both userPhone and userEmail values (both matching and non-matching). All work as expected, denying mismatch values and allowing matched values.
This is where is gets strange. When I run this (Android) query:
val companyMemberDocuments = Firebase.firestore.collection("PendingInvites").whereEqualTo("userPhone", firebaseAuth.currentUser!!.phoneNumber).whereEqualTo("userEmail", firebaseAuth.currentUser!!.email).get().await()
I get "PERMISSION_DENIED: Missing or insufficient permissions". As near as I understand rules, I think it should work. It works in the online console, I'm specifically querying for userPhone (or userEmail) but it doesn't work.
I've tried removing the userEmail and only testing for userPhone, but that also appears to not work.
Any ideas how I can correct the rules (or query)?
Thanks
Document:
Auth token used in testing:
{
"uid": "",
"token": {
"sub": "",
"aud": "certifly-global",
"phone_number": "+16505551234",
"firebase": {
"sign_in_provider": "phone"
}
}
}
Note how the document includes BOTH, userPhone and userEmail
Your rules are rejecting the query every time because Firestore security rules are not filters. Be sure to read that documentation and this post.
Your query is asking for all documents in PendingInvites where userPhone == +16505551234. However, your rules do not allow this for two reasons:
Your query doesn't match the constraints of rule (that both userPhone and userEmail are set to specific values). The query is just specifying userPhone.
The system will not perform a get() for every single document that could match. While this will work for individual document gets from the client, it will not work for queries that could return any number of documents.
So you will have to resolve both problems.
Your query will need to filter using both userPhone and userEmail, as required by your rules. This means you will have to add another whereEqualTo on userEmail that matches the requirement of the rules. In other words, the client app needs to pass the user's email in that filter.
You don't need to use a get() at all in the rules. You can refer to fields in documents in the current collection via resource.data.
The rules will need too something more like this:
match /PendingInvites/{inviteID} {
allow read: if
request.auth != null &&
resource.data.userPhone == request.auth.token.phone_number &&
resource.data.userEmail == request.auth.token.email;
}

Firestore rules allow subcollection

My db structure is like this:
//Sub collections
/inventory/{inventoryId}/armor/chest/
/inventory/{inventoryId}/armor/head/
...
// Document
/inventory/{inventoryId}.ownerUID // ownerUID = firebaseID
/inventory/{inventoryId}.charName // Character name that owns this inventory, each user can own multiple characters, each character has one inventory linked to it
Probably not relevant:
/characters/{charName}.ownerUID
/characters/{charName}.charName
/characters/{charName}.inventoryID
I'm trying to write the rules so each user can only read/write inventories that belong to him, for the top document in inventory I can just write something like:
match /inventory/{inventoryID}/{document=**} {
allow read,write: if request.auth != null && resource.data.ownerUID == request.auth.uid
}
However, this will fail for nested collection as the resource.data.ownerUID only exists at the top level.
Is there a way I can get {inventoryID} from /inventory/{inventoryID}/{document=**} and check it against firebaseID or maybe somehow use the data from /character/
Is my only option adding ownerUID to every subcollection of /inventory?
If you need to use fields from other documents than the one that matches the match pattern, you can use get() to read that document and use its fields. For example:
match /inventory/{inventoryID}/{document=**} {
allow read, write: if
get(/databases/$(database)/documents/inventory/$(inventoryID)).data.ownerUID
== request.auth.uid;
}

Firestore Security Rules for Query with Array Contains

I have a Flutter app in which users can make posts and tag the post as belonging to a group. Posts are stored in a global collection and each has a Post.groupId field:
/posts/{postId}
Based on my Firestore security rules and queries, users are only allow to read posts if they are in the group for which the post is tagged (i.e the posts's groupId field). Approved group users are stored in:
/groups/{groupId}/users/{userId}
I could query the posts from a particular user's group like:
_firestore.collection('posts').where('groupId', isEqualTo: 'groupA')...
This above was all working properly.
I am attempting to make an improvement in which a post can be tagged in multiple groups instead of just one, so I am replacing the single Post.groupId field with a Post.groupIds array. A user should be able to read a post if he/she is a member of ANY of the groups from Post.groupIds. I attempt to read all posts tagged with a particular group with the following query from my Flutter app:
_firestore.collection('posts').where('groupIds', arrayContains: 'groupA')...
I keep receiving the following exception Missing or insufficient permissions with these security rules:
match /posts/{postId} {
allow read: if canActiveUserReadAnyGroupId(resource.data.groupIds);
}
function isSignedIn() {
return request.auth != null;
}
function getActiveUserId() {
return request.auth.uid;
}
function isActiveUserGroupMember(groupId) {
return isSignedIn() &&
exists(/databases/$(database)/documents/groups/$(groupId)/users/$(getActiveUserId()));
}
function canActiveUserReadAnyGroupId(groupIds) {
return groupIds != null && (
(groupIds.size() >= 1 && isActiveUserGroupMember(groupIds[0])) ||
(groupIds.size() >= 2 && isActiveUserGroupMember(groupIds[1])) ||
(groupIds.size() >= 3 && isActiveUserGroupMember(groupIds[2])) ||
(groupIds.size() >= 4 && isActiveUserGroupMember(groupIds[3])) ||
(groupIds.size() >= 5 && isActiveUserGroupMember(groupIds[4]))
);
}
With these security rules I can read a single post but I cannot make the above query. Is it possible to have security rules which allow me to make this query?
UPDATE 1
Added isSignedIn() and getActiveUserId() security rules functions for completeness.
UPDATE 2
Here is the error I am receiving when I attempt to execute this query with the Firestore Emulator locally:
FirebaseError:
Function not found error: Name: [size]. for 'list' # L215
Line 215 corresponds to the allow read line within this rule:
match /posts/{postId} {
allow read: if canActiveUserReadAnyGroupId(resource.data.groupIds);
}
It appears Firestore does not currently support security rules for this scenario at the moment (thanks for your help tracking this down Doug Stevenson). I have come up with a mechanism to work around the limitation and wanted to share in case someone else is dealing with this issue. It requires an extra query but keeps me from having to create a Web API using the Admin SDK just to get around the security rules.
Posts are stored as follows (simplified):
/posts/{postId}
- userId
- timestamp
- groupIds[]
- message
- photo
Now I am adding an additional post references collection which just stores pointer information:
/postRefs/{postId}
- userId
- timestamp
- groupIds[]
The posts collection will have security rules which does all the validation to ensure the user is in at least one of the groups in which the post is tagged. Firestore is able to handle this properly for simple get requests, just not list requests at the moment.
Since the postRefs collection stores only ID's, and not sensitive information which may be in the post, its security rules can be relaxed such that I only verify a user is logged in. So, the user will perform post queries on the postRefs collection to retrieve a list of ordered postId's to be lazily loaded from the posts collection.
Clients add/delete posts to/from the normal posts collection and then there is a Cloud Function which copies the ID information over to the postRefs collection.
As per this blog post, if you can maintain an index of member IDs for a given post (based on group assignments), then you can secure post read access storing member IDs in an array data type and matching against the member IDs with the "array-contains" clause in your ruleset. It looks like this in your Firebase rules:
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{postId} {
allow read: if request.auth.uid in resource.data.members
allow write: if request.auth.uid == resource.data.owner
}
}
}
If I had to guess, I'd say that groupIds isn't actually a List type object, which means that the field from the document is also not an array. If it's a string, this code won't work, since strings don't have a method called size() in the rules language.
If you aren't 100% certain what the type of field is going to be, you will need to check the type in the rule and determine what to do with it. You can use the is operator to check the type. For example, groupIds is list will be boolean true if you're actually working with one.
In your rules, you can use the debug() function to dump the value of some expression to the log. It will return the same value. So, you can say debug(groupIds) != null to both print the value and check it for null.

Firestore security rule: How to ensure uniqueness of values in document?

Can the below security rule ensure uniqueness of firstName, lastName, username, and email before creating a document in profiles collection?
match /profiles/{document=**} {
allow create: if request.auth.uid != null
&& (request.resource.data.firstName is string && resource.data.firstName != request.resource.data.firstName)
&& (request.resource.data.lastName is string && resource.data.firstName != request.resource.data.firstName)
&& (request.resource.data.username is string && resource.data.username != request.resource.data.username)
&& (request.resource.data.email is string && resource.data.email != request.resource.data.email)
}
For example, below is the data in Firestore collection profiles
{
"document1":{
"firstName":"Jek",
"lastName":"Choo",
"email":"jeksomething#gmail.com",
"username":"jek"
},
"document2":{
"firstName":"Cara",
"lastName":"Choo",
"email":"babycara#gmail.com",
"username":"cara"
}
}
I want to create the below new document, and this create access should be denied
{
"document3":{
"firstName":"Jek",
"lastName":"Choo",
"email":"jeksomething#gmail.com",
"username":"jek"
}
}
And I want to create the below new document, this should be allowed.
{
"document4":{
"firstName":"example",
"lastName":"com",
"email":"test#example.com",
"username":"example"
}
}
In conclusion, can the above firestore security rule help to ensure field value uniqueness before a document is allowed to be created?
It's important to understand what resource does with respect to rules that create new documents, which is the only rule you're showing here.
resource refers to "the (existing) document being written". This is in contrast with request.resource which describes the document that doesn't yet exist, that is about to exist, if the write succeeds.
To put it another way, in this section:
The resource variable refers to the requested document, and
resource.data is a map of all of the fields and values stored in the
document.
In the case of a create, there is no existing document being written. Therefore, you can assume that any matches against resource to fail. Therefore, this will not ensure uniqueness.
In fact, you can not ensure uniqueness of any given document field for a create, since it's not possible in security rules to query all documents in the collection for existence for that field.
The only form of uniqueness observed by Firestore is that of the id of a document within a collection. All fields of that document can not be constrained to be unique by security rules, and there are no indexes in Firestore that ensure uniqueness.
If you need a field to be unique, you should check after the document creation by using Cloud Function trigger, then delete the document if it doesn't satisfy the requirements.

Firestore rule to check for field existence results in missing or insufficient permissions error

I have a read rule on my firestore database as follows:
match /messages/{document=**} {
allow read: if (resource.data.account_id == request.auth.uid
&& resource.data.keys().hasAll(['is_note']))
|| request.auth.token.role.FirebaseAdminIssuer == true;
}
I believe this should return the user all documents where the account_id field matches their uid and where there the document has a field called is_note.
If I just have the first rule:
match /messages/{document=**} {
allow read: if resource.data.account_id == request.auth.uid
|| request.auth.token.role.FirebaseAdminIssuer == true;
}
This accurately gives me the documents where the account_id field matches the user uid.
However, when I have manually add a boolean field called is_note to some (not all) documents and set the rule to restrict to documents that also include that, I get Error: Missing or insufficient permissions.
Is there something wrong with my rule?
FYI I have used the documentation here to determine rule to use: https://cloud.google.com/firestore/docs/reference/security/#firestore-resource

Resources