See Array Changes in Firestore Security - firebase

I have a collection reviews where each review contains a list of uids of users who have liked it called likes. The schema looks like:
review (collection)
title string
author uid
likes [uid]
posted timestamp
user (collection) - uid
created timestamp
email string
Currently, I'm handling a user liking a review with:
firebase.firestore().doc(rid).update({
likes: firebase.firestore.FieldValue.arrayUnion(this.fetchCurrentUID())
});
And unliking with:
firebase.firestore().doc(rid).update({
likes: firebase.firestore.FieldValue.arrayRemove(this.fetchCurrentUID())
});
I only want to let a user add or remove their own uid from likes.
How can I write a security rule to ensure this? Specifically, I need to see how the list is being updated, for instance something like:
let newVals = request.resource.data.new_values // or something
return (newVals.length == 1 && newVals[0] == request.auth.uid)

Instead of checking the value of the arrayUnion we are updating with, we can check the the final result.
request.resource.data.likes Is the array AFTER the arrayUnion has been applied.
Therefore liking should result in request.auth.uid in request.resource.data.likes == true
For unliking we negate it !(request.auth.uid in request.resource.data.likes) == true
This is easy because we have access to request.auth.uid but in cases where we don't have the supposed value accessible this way. The following answer has a solution:
Firebase security rules, ensure one "array remove" only, and only to userId
It uses the following trick - create an array intersection with removeAll:
resource.data.likes.removeAll(request.resource.data.likes)[0] == request.auth.uid

Related

Can a user read a collection of users in firestore from frontend?

I am saving the below Data in the user's collection in firebase
{
"uid":"randomid",
"name":"name",
"number":"1234"
}
when I try to check if the user exists the below code works ok
const result = await firestore().collection('users').where('uid', '==', userid).get()
so can an authenticated user read the whole users' collections?
const result = await firestore().collection('users').get()
What security rules I can write to prevent users from reading a collection but only reading their info based on uid?
In security rules you can split the read access to get and list. So if you want the give access to each user to get only his own data you need to use the following rule (I assume each user document in the collection is the uid of this user):
match /users/{user} {
function isUserOwner() {
return request.auth.uid == user
}
allow get: if isUserOwner();
allow list: if false;
}
First you need to set the uid field to the UID of the user who created the document.
To get the current user id See documentation
const uid = user.uid;
To add the currently logged in User id as a field visit stack overflow example link for javascript
After adding UID you can use request.auth and resource.data variables to restrict read and write access for each document to the respective users. Consider a database that contains a collection of story documents. Have a look at below example
{
title: "A Great Story",
content: "Once upon a time...",
author: "some_auth_id",
published: false
}
You can use below security rule to restrict read and write access for each story to its author:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{storyid} {
// Only the authenticated user who authored the document can read or write
allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
}
}
}
Note that the below query will fail for the above rule even if the current user actually is the author of every story document. The reason for this behavior is that when Cloud Firestore applies your security rules, it evaluates the query against its potential result set, not against the actual properties of documents in your database
// This query will fail
db.collection("stories").get()
The appropriate query for the above rule is
// This query will work
var user = firebase.auth().currentUser;
db.collection("stories").where("author", "==", user.uid).get()
For additional information on the above rules and query see official documentation

Firestore Database security Rules: Problem with collection restriction based on array from another collection

we got some problems with our security rules settings.
Our code looks like this
service cloud.firestore {
match /databases/{database}/documents {
function sameCompany(){
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.companyId == resource.data.companyId
&& request.auth.uid in get(/databases/$(database)/documents/companies/$(resource.data.companyId)).data.users;
}
match /orders/{orderId} {
allow read: if request.auth != null && sameCompany();
}}
The collections have the following structure:
orders/{orderId}
users/{userId}
companies/{companyId}
each doc in orders has one companyId.
each doc in users has one companyId.
each doc in companies has an array containing the userIds that are in the company
Only users that have the same companyId as the order and also if their userId is in the array from the company should be able to read the orders.
This is to have a double security that users cant change their companyId to read orders from another company
The first part before the && works just fine. If I manually put an Array after "request.auth.uid in " that contains the userId that is querying it also works fine, but somehow it doesnt work like this and I cant find the issue here.
Can someone help? Thank you for your time!
Edit1:
Thank you for your responses so far.
So the query looks like this:
query(
collection(db, "orders"),
where("companyId", "==", user.companyId)
)
We save in local Storage the user with the companyId. Without the second part after the && the data is retrieved like it should be, but with it nothing is shown. We want to double check if the users has the same companyId and is in the users array in the company in case someone tries to guess the companyId from another company and somehow can access the data. Like this you would need to know the companyId and the userId.
The data could simply look like this:
users doc with uId1:
{companyId: cId1, ...}
orders doc with dId1:
{companyId: cId1, ...}
companies with cId1:
{users: [uId1], ...}

Implement Firestore security rules for a many to many relationship

I'm struggling use Firestore security rules to secure a many to many relationship.
I have the following collections:
Key:
documentId: [field: value, ...]
groups {
group1: [name: Group1]
group2: [name: Group2]
}
users {
bobUser: [name: Bob]
aliceUser: [name: Alice]
fredUser: [name: Fred]
}
// Contains data specific to a user in a particular group.
// Specifically the user's role
userGroups {
userGroup1: [userId: bobUser, groupId: group1, role: "admin"]
userGroup2: [userId: aliceUser, groupId: group1, role: "member"]
userGroup3: [userId: fredUser, groupId: group2, role: "admin"]
}
How can I construct a firestore security rule so that:
A user with role:"admin" can read another user's document if they both are found in the same group
So in the example above, Bob can read Alice's user document as he has an "admin" role but Fred can't as he is an admin for another group.
Or to put it another way:
If bobUser makes the below request, then it should pass security rules:
db.collection("users").doc("aliceUser");
as bob is has an admin role in the same group as Alice
In contrast, if fredUser was logged in, the below request would fail:
db.collection("users").doc("aliceUser");
Fred is an admin user, but not in the same group and so the rule would block the request.
In the security rule I think I need to split into a few stages:
Query userGroups to find all groupIds where requesting userId is role: "admin"
Query userGroups to find all groupIds where requested user exists
Allow write if there is a match of groupIds in both groups
But I'm having trouble getting this logic into the rule. Security rules don't seem be able to filter like this. Any help would be great!
In order to solve this problem, you need to keep in mind that you cannot transfer relational database patterns to a non-relational database. When working with Firestore, you should start by asking "What queries should be possible?" and then structure your data based on that. Building up Security Rules will follow naturally.
I wrote a blogpost about exactly your use case: "How to build a team-based user management system with Firebase", so if anything from the following answer is unclear, go there first to see if it helps.
In your case, you'd probably want the following queries:
Get all users of a group (given the current user is a member of this group and has the correct permission).
Get all groups of a user (given you are only querying groups of the currently authenticated user).
As you noticed, many-to-many relationships are hard to work with through Firestore and Security Rules, because you would need to make additional requests to join the datasets. To avoid that, I recommend renaming the userGroups collection to memberships and turning it into a subcollection of each doc in the groups collection. So your new structure would look like
- collection "groups", a doc contains:
- field "name"
- collection "memberships", a doc contains:
- field "name"
- field "role"
- field "user" → references doc from "users"
- collection "users", a doc contains:
- field "name"
This way you can easily solve the first query "Get all users of a group" by querying the subcollection "memberships" like collection("groups").doc("your-group-id").collection("memberships").get().
Now, to secure that, you can write a helper functions in Security Rules:
function hasAccessToGroup(groupId, role) {
return get(/databases/$(database)/documents/groups/$(groupId)/memberships/$(request.auth.uid)).data.role == role
}
Given a groupId and a role, you can use it allow only users who are a member and have a specific role access to data and subcollections within the group. In order to protect the memberships collection on a group this might look like this:
rules_version = '2'
service cloud.firestore {
match /databases/{database}/documents {
function hasAccessToGroup(groupId, role) {
return get(/databases/$(database)/documents/groups/$(groupId)/memberships/$(request.auth.uid)).data.role == role
}
match /groups/{groupId} {
// Allow users with the role "user" access to read the group doc.
allow get: if
request.auth != null &&
hasAccessToGroup(groupId, "user")
// Allow users with the role "admin" access to read all subcollections, including "memberships".
match /{subcollection}/{document=**} {
allow read: if
request.auth != null &&
hasAccessToGroup(groupId, "admin")
}
}
}
}
Now there is only the second query "Get all groups of a user" left. This can be achieved through a Collection Group Index, which allows to query all subcollections with the same name. You want to create one for the memberships collection. Given a specific user, you can then easily query all of his groups with collectionGroup("memberships").where("user", "==", currentUserRef).get().
In order to secure that, you need to setup a Rule that allows such requests only if the queried user reference equals the currently authenticated user:
function isReferenceTo(field, path) {
return path('/databases/(default)/documents' + path) == field
}
match /{document=**}/memberships/{userId} {
allow read: if
request.auth != null &&
isReferenceTo(resource.data.user, "/users/" + request.auth.uid)
}
One last thing to talk about is how you keep the data in the memberships collection up-to-date with the data in the users doc that it references. The answer are Cloud Functions. Every time a users doc changes, you query all of its memberships and update the data.
As you can see, answering your original question how you can construct a Firestore Rule so that a user with the correct permission can read another user's document if they both are found in the same group, takes a different approach. But after restructuring your data, your Security Rules will be easier to read.
I hope this helped. Cheers!

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 get field/id of reference

I have two collections - tenancies and users.
A tenancy doc has a field called "landlordID" and is of type REFERENCE (not String).
Now in my Firestore Security Rules I want to allow a tenancy to be updated ONLY IF the landlordID field of that tenancy matches with the uid of the user making the request, namely request.auth.uid.
Read it as " allow a tenancy document to be updated if the user making the user is authenticated, hence request.auth.uid != null, and the landlordID field's ID should be equal to that of the request.auth.uid.
Hence the code should me something like this:
service cloud.firestore {
match /databases/{database}/documents {
match /tenancies/{tenancyID}{
allow update: if request.auth.uid != null &&
request.auth.uid == get(resource.data.landlordID).id
}
}
I have also tried get(/databases/$(database)/documents/users/$(resource.data.landlordID)).data.id
Supporting screenshot of my database
This should be very simple but get() simply does not work. Firebase Docs, scroll to "Access other documents" was not helpful at all for my situation and I am not sure how to get it working.
It would be a shame if references can't be used like this as they are just like any other field of a document.
Here is a function I made that works for me. I guess you have a user collection with users having the same id as their auth.uid
function isUserRef(field) {
return field in resource.data
&& resource.data[field] == /databases/$(database)/documents/users/$(request.auth.uid)
}
Adjusting to your use case you'd call the function so: isUserRef('landlordID') although the ID at the end of it is a bit misleading as this field is in fact a reference.
I see a couple of issues here. A first problem is that the get() function expects a fully specified ducument path, something like:
get(/databases/$(database)/documents/users/$(resource.data.landlordID)).data.id
A second problem is that you are trying to use the reference type in your rules, I do not think that is possible unfortunately.
The reference type in Firestore is not very helpfull (yet), I think you should store the landlordID as a string, then you can simply do something like:
service cloud.firestore {
match /databases/{database}/documents {
match /tenancies/{tenancyID}{
allow update: if request.auth.uid != resource.data.landlordID;
}
}
I had the same issue I needed an answer for. See this Google-thread with the answer from someone from google. To quote it:
You can get an id out of a path using the "index" operator:
some_document_ref should look like /databases/(default)/documents/foo/bar
which has 5 segments: ["databases", "(default)", ...]
some_document_ref[4] should be "bar"
allow create: if request.resource.data.some_document_ref[4] == "bar";
You can also use the normal get and exists functions on them.
A few difficult aspects of this that you may run into:
There's no way to retrieve the number of segments in a path at the moment (we're adding this soon), so you'll need to know some information about the reference ahead of time
There's not great support for writing references using the simulator in the Firebase Console. I used the Firestore emulator to test out this behavior (gist1, gist2)
might be too late, but I was able to piece together (despite a lack of docs) that a document reference is just a path, and complete path can be created with
/databases/$(database)/documents/users/$(request.auth.uid)
Then I have an array/list in firestore of references, called reads that I can grab with:
get(/databases/$(database)/documents/users/$(userId)/userinfo/granted_users).data.reads
Leaving me able to create a bool, and a rule with:
/databases/$(database)/documents/users/$(request.auth.uid) in get(/databases/$(database)/documents/users/$(userId)/userinfo/granted_users).data.reads
obviously your data structure will vary, but knowing the ref is a path is the important part here.
I had to experiment a little to get this working. Here the function that worked for me
function isUserRef(database, userId) {
return 'user' in resource.data
&& resource.data.user == /databases/$(database)/documents/users/$(userId);
}
And I call it like:
match /answers/{answer} {
allow read:
if isUserRef(database, request.auth.uid);
}
As mentioned by some other answers, a reference has a path property that is just a string that will look something like users/randomuserid123. You can split that into an array and match it against the user making the update request.
...
match /tenancies/{tenancyID}{
allow update: if request.auth.uid != null &&
resource.data.landlordID.path.split('/') == ['users', request.auth.uid]
}
...
Also had a trouble handling this problem, but in my case I needed to allow the user to add a message into a chat only if they're the owner of that chat room. There are 2 "tables" - chats and chat_messages, and chat_messages relate to a specific chat through chatId field. chats objects have ownerId field.
The rule I've used goes like this:
// Allow adding messages into a chat if the user is an owner of the chat room
match /chat_messages/{itemId} {
function isOwner() {
return get(/databases/$(database)/documents/chats/$(request.resource.data.chatId)).data.ownerId == request.auth.uid;
}
allow read: if true;
allow create: if isOwner();
}

Resources