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

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.

Related

Firebase security rules resource.data not working

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.

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;
}

how to allow an access even if the document field is not available in firestore security rules?

so I add a new field in my user document called numberOfFollowers, so the brand new user will have this field but, old user doesn't has this numberOfFollowers field.
in security rules I want to set that the numberOfFollowers should be a number. and if old user does not have this field then it still allowed to update their document.
so I set the security rules like this
match /users/{userID} {
allow update: if isValidUserStructure(request.resource.data)
}
function isValidUserStructure(user) {
return (user.numberOfFollowers is number || user.numberOfFollowers == null)
&& user.banned is bool
&& user.contactNumber is string
&& user.createdAt is timestamp
&& user.domicile is string
&& user.email is string
}
but the access is always denied for old user that doesn't has this field because of this line (user.numberOfFollowers is number || user.numberOfFollowers == null)
To check if a field is NOT present on some document, you should check the data object, which is a Map type object, using the documented in operation that checks for presence of a key:
function isFieldPresent(data, fieldName) {
return fieldName in data;
}
You can then choose what to do in this case.
Since null is a valid field value in Firestore, checking for null as you're doing now is definitely not the same thing as checking for non-existence.

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 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