Firestore rules allow subcollection - firebase

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

Related

How to add a Firebase security rule for checking if logged in users email is in a list

I try to add a security rule that should grant write access given that the logged in user is in a list within the record.
I.e. my "tournament" record has a List officials in where I put the emails of those that should be able to alter the tournament.
I don't get this to work so I guess that my rule has some error in it.
match /tournaments {
allow read: if request.auth.uid != null;
allow write: if request.auth.token.email in resource.data['officials'];
}
The rule you have written is has some missing syntax. The rule must be like mentioned below:
match /tournaments/{tournamentID} {
allow read: if request.auth.uid != null;
allow write: if request.auth.token.email in resource.data['officials'];
}
The /tournaments/{tournamentID} indicates that the rule will apply for all documents present in tournament collection and {doc_id} is a wild card to representing documents in that collection.

Is it possible to create firestore rules based on specific fields?

I'm going to try to generalise a bit here to make the question simpler. I have a Firestore database that stores users and forum posts. I'm trying to write the rules for the forum posts specifically.
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null
}
function isOwner(userID) {
return request.auth.uid == userID
}
// ...
match /forum/{topicID} {
allow read: if true
allow create: if isSignedIn()
allow update, delete: if isSignedIn()
}
}
So here, I've got it so that you can create, update and delete if you are signed in. I also need to add the check, "Do they own this post?", with the isOwner() function.
That's all chill, but my problem is - each forum post (AKA topic) has two fields in them that can be updated by anybody who is signed in. These fields are likes and likeCount.
likes is an array of strings, each string is a user's id. likeCount is a number equal to the likes array length.
I'm starting to see that this would have been easier if likes was a sub-collection and likeCount could just be likesSnapshot.docs.length. I just fear the amount of recoding that might involve!
So, long story short, I want to do something along the lines of:
match /forum/{topicID} {
allow read: if true
allow create: if isSignedIn()
allow update, delete: if isSignedIn() && isOwner(resource.data.user.id)
match /likes && /likeCount {
allow create, update: if isSignedIn()
allow delete: if isSignedIn() && isOwner([THE USER ID THAT IS BEING REMOVED FROM THE ARRAY])
}
}
...but I'm not sure of the best way to go about it!
Pleases and thank yous in advance :)
It's not possible to target specific fields like you are showing in security rules. You can only match whole documents with a match statement.
If you want make sure certain users can only modify certain fields, you can use the MapDiff api to check that only those fields are being changed in the document data. It will go something like this:
if isSignedIn() &&
request.resource.data.diff(resource.data).affectedKeys().hasOnly(["likes", "likeCount"])
This will evaluate true if only likes and likesCount are being modified, and the user is signed in.
Yes you can do this. I actually did it recently in my own forum.
The way to do it is to allow update if isOwner and restrict what non owners are able to update. You can do that like this:
allow update: if request.auth.uid != null
&& request.resource.data.{some field} == resource.data.{some field}
&& request.resource.data.title.{some other field} == resource.data.{some other field}
Put all data fields that you don't want non owners to be able to update in the {some field} placeholder. The rule is checking that certain fields have not changed and thus restricting what a user is allowed to change in a document.
Then allow owners full update privs:
allow update: if isOwner()

Firestore rules based on path component and other collection

I am trying to establish a rules, where user would only be able to perform operations on Chat messages after they have made a purchase of one of the services.
My database structure looks like this:
I have purchases collection: purchases/{purchaseId} which contains buyer_id field.
I also have messages collection: /channels/{purchaseId}/thread/{threadId}.
I want to allow CRUD operations if the users in thread are the same as buyer_id from purchases collection, in purchaseId document.
This is what I've tried, using current user's ID (auth) for now. This doesn't work, either.
Ideally, I would substitute request.auth.uid with the field from the document I am trying to access, but this would do for now.
match /channels/{purchaseId}/thread/{threadId} {
allow read, create, update, delete: if get(/databases/{database}/documents/purchases/{purchaseId}).data.buyer_id == request.auth.uid;
}
I am getting the standard permissions error. What am I missing?
You syntax is wrong when defining the path. Try this:
match /channels/{purchaseId}/thread/{threadId} {
allow read, write: if get(/databases/$(database)/documents/purchases/$(purchaseId)).data.buyer_id == request.auth.uid;
}
Substituting request.auth.uid with the field from the document:
match /channels/{purchaseId}/thread/{threadId} {
allow read, write: if get(/databases/$(database)/documents/purchases/$(purchaseId)).data.buyer_id == resource.data.buyer_id;
}

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 Rules: Can't understand why certain rules do/don't work

My firestore database is structured so that "leagues" is the top collection, and each league contains a field named after each approved userID (with a number value).
Each league also has a subcollection "users" of documents named after each approved userID.
Here is an example firestore query:
FirebaseAuth mAuth = FirebaseAuth.getInstance();
// userId = ABCDEF123 for this example
String userId = mAuth.getCurrentUser().getUid();
FirebaseFirestore firestore = FirebaseFirestore.getInstance();
firestore.collection("leagues").whereLessThan(userId, 99).get();
Can someone please tell me why this rule works:
match /leagues/{league} {
allow read, write: if resource.data.ABCDEF123 != null;
but not this:
match /leagues/{league} {
allow read, write: if resource.data.request.auth.uid != null;
Also, why does this rule work:
//"ZYXWV987" is an example of a league the user is in
match /leagues/{league} {
allow read, write: if exists(/databases/$(database)/documents/leagues/$('ZYXWV987')/users/$(request.auth.uid));
but not this:
match /leagues/{league} {
allow read, write: if exists(/databases/$(database)/documents/leagues/$(league)/users/$(request.auth.uid));
The error I get is "com.google.firebase.firestore.FirebaseFirestoreException: PERMISSION_DENIED: Missing or insufficient permissions"
I am looking to understand how these rules work, and consequently how to implement proper rules for my database!
EDIT:
I now realize that this works (still a WIP):
match /leagues/{league} {
allow read, create, update: if request.auth.uid != null;
//only ever deleting a single league at a time
allow delete: if exists(/databases/$(database)/documents/leagues/$(league)/users/$(request.auth.uid));
match /{docs = **} {
allow read, write: if exists(/databases/$(database)/documents/leagues/$(league)/users/$(request.auth.uid))}
}
and I sort of understand what's going on (I can't use the {league} wildcard when reading/writing potentially more than one league in a request?), but I'm still not exactly sure why?
Can someone please tell me why this rule works:
match /leagues/{league} {
allow read, write: if resource.data.ABCDEF123 != null;
but not this:
match /leagues/{league} {
allow read, write: if resource.data.request.auth.uid != null;
I guess there is no field named request in resource.data. How should the dot syntax know that you want to evaluate the last part (request.auth.uid) first and use the result as the next key? I'm not sure but you could try resource.data[request.auth.uid] instead.
Also, why does this rule work:
// "ZYXWV987" is an example of a league the user is in
match /leagues/{league} {
allow read, write: if exists(/databases/$(database)/documents/leagues/$('ZYXWV987')/users/$(request.auth.uid));
but not this:
match /leagues/{league} {
allow read, write: if exists(/databases/$(database)/documents/leagues/$(league)/users/$(request.auth.uid));
From the docs on security rules:
Every database request from a Cloud Firestore mobile/web client
library is evaluated against your security rules before reading or
writing any data.
The first rule is evaluated and depending on the result, the user is allowed to read ALL or NONE league documents.
The second rule would have to be evaluated for every single document depending on the content. That is not possible before reading them all.
You have to define your security rules in a way that Firestore can evaluate them based only on the definition of your query independent of the possible result.

Resources