Cloud Firestore: Query secured documents by field content - firebase

EDIT: Seems to be an open issue in Firestore. Also see this post.
In Google Cloud Firestore, I want to model a collection of groups. Each group contains a name, the list of it's users and some secretGroupData. For me, the natural way to do this would be:
/groups
/group1 {
name: "Group 1"
users: { //object can be queried, simple array not
"user1": true,
"user5": true
}
secretGroupData: ...
}
/group2 { ... }
Given a user like user1, I want to query all groups he is member of. This query works fine:
groupsRef.where("users.user1", "==", true)
However, I want to secure the group data. This query only works, when all groups are readable for all users. When I protect the group to be readable only by the group members, by the rule
match /groups/{groupId} {
allow read: if resource.data.users[request.auth.uid] == true;
}
the above query does not work any more, because as soon as it sees a group where the current user is not a member of, read access is denied and the whole query fails.
What is the best solution for this problem in Firestore? Should I
tell Firestore to return only the allowed groups and ignore the other ones, instead of throwing an error? If so, how can I achieve this?
make the groups readable for all users and move the secretGroupData into subcollections, where I can then restrict the access to just the group members
add redundancy by adding the IDs of all groups of a user into the user's profile document (/users/user1/groupIds: ["group1"]), so I know the groups beforehand and can query them by ID
use a totally different solution?
Thank you very much for your ideas.

Related

Firebase security rules based on custom user field

I'm trying to set up security rules (use Firebase Cloud Firestore).
I changed the "users" table (added company_id field) and create "appointments" table (with company_id). I want to implement the following functionality (when a user requests appointments, he only receives appointments with his company id)
Wrote a rule:
match /appointments/{appointment} {
allow write;
allow read, update, delete: if resource.data.company_id == get(/databases/$(database)/documents/users/$(request.auth.uid)).data.company_id;
}
But my code throw error about permissions
const q = query(collection(db, 'appointments'), where("company_id", "==", company_id), orderBy("createdAt"));
Firestore doesn't magically know what documents have what company_id, so it tries to read all documents to return the documents that you want, but it can't, your security rules stop it.
Either get rid of the security rules, or find a way to structure your database so that you don't have this problem.
I don't know enough about your database to tell you a good structure, but what about something like this?
A collection called companies.
Each document in the collection is a company_id
A subcollection called users, where you put all users that belong to this company.
Another subcollection, called appointments, where you put all appointments for the company.
That way, you could write a security rule like so:
match /companies/{company}/appointments/{appointment} {
allow create: if true;
allow read, update, delete: if exists(/databases/$(database)/documents/companies/$(company)/users/$(request.auth.uid))
}
The problem with this approach is that, because we have divided all users and appointments into different collections, it will be impossible to query for all existing users or for all existing appointments.
It all depends on your use case.

Cloud Firestore rule for Read - on the basis of where clause [duplicate]

I'm trying to secure requests to a collection to allow any single get, but to allow list only if a specific key is matched.
Database structure is like this:
posts
post1
content: "Post 1 content"
uid: "uid1"
post2
content: "Post 2 content"
uid: "uid1"
post3
content: "Post 3 content"
uid: "uid2"
The Firestore query I'm making from Vue:
// Only return posts matching the requested uid
db
.collection("posts")
.where("uid", "==", this.uid)
The security rules I'd like to have would be something like this:
match /posts/{post} {
allow get: if true // this works
allow list: if [** the uid in the query **] != null
I want to do this so you can list the posts of a specific user if you know their uid but can't list all posts of the system.
Is there a way to access the requested .where() in the security rules or how can I write such rule or structure my data in this case?
Relevant & credits:
Seemingly, I can make a request on a query's limit, offset, and orderBy. But there's nothing on where. See: #1 & #2.
I copy-pasted much from this question. I don't see how the accepted answer answers the question. It seems like it answers another case where a user is allowed to list some other users' posts. That is not my case; in my case, what's public is public. So, it doesn't answer the main question in my case, it seems.
There's currently no way, using security rules, to check if a field is being used in query. The only thing you can do is verify that a document field is being used as a filter using only values you allow.
Instead, consider duplicating enough data into another collection organized like this:
user-posts (collection)
{uid} (document using UID as document ID)
posts (subcollection)
{postId} (documents using post ID as document ID)
This will require the client to call out a UID to query in order to get all the posts associated with that user. You can store as much information about the post documents as you like, for the purpose of satisfying the query.
Duplicating data like this is common in NoSQL databases. You might even want to make this your new default structure if you don't want your users to query across all posts at any given moment. Note that a collection group query naming the "posts" subcollection would still query across all posts for all users, so you'd have to make sure your security rules are set up so that this is enabled only when you allow it to happen.
Also note that UIDs are typically not hidden from users, especially if your web site is collaborative in nature, and you combine multiple users' data on a single page.

Firebase database check if element exists in a ListField in Flutter

I have a real-time database on firebase which consists of ListFields. Among these fields, one field, participants is a list of strings and two usernames. I want to make a query to firebase database such that it will return the documents in which a particular username is present in the participants list.
The structure of my document is as follows :
I want to make a query such that Firebase returns all the documents in which the participants list consists aniruddh. I am using Flutter with the flutterfire plugins.
Your current data structure makes it easy to find the participants for a conversation. It does however not make it easy to find the conversations for a user.
One alternative data structure that makes this easier is to store the participants in this format:
imgUrls: {},
participants: {
"aniruddh": true,
"trubluvin": true
}
Now you can technically query for the the conversations of a user with something like:
db.child("conversations").orderByChild("participants/aniruddh").equalTo(true)
But this won't scale very well, as you'll need to define an index for each user.
The proper solution is to add a second data structure, known as an inverted index, that allows the look up of conversations for a user. In your case that could look like this:
userConversations: {
"aniruddh": {
"-LxzV5LzP9TH7L6BvV7": true
},
"trubluvin": {
"-LxzV5LzP9TH7L6BvV7": true
}
}
Now you can look up the conversations that a user is part of with a simple read operation. You could expand this data structure to contain more information on each conversation, such as the information you want to display in your list view.
Also see my answer heres:
Firebase query if child of child contains a value (for more explanation on why the queries won't work in your current structure, and why they won't scale in the first structure in my answer).
Best way to manage Chat channels in Firebase (for an alternative way of naming the chat rooms).

Firebase Firestore - security rule based on "where" query parameters

I'm trying to secure requests to a collection to allow any single get, but to allow list only if a specific key is matched.
Database structure is like this:
posts
post1
content: "Post 1 content"
uid: "uid1"
post2
content: "Post 2 content"
uid: "uid1"
post3
content: "Post 3 content"
uid: "uid2"
The Firestore query I'm making from Vue:
// Only return posts matching the requested uid
db
.collection("posts")
.where("uid", "==", this.uid)
The security rules I'd like to have would be something like this:
match /posts/{post} {
allow get: if true // this works
allow list: if [** the uid in the query **] != null
I want to do this so you can list the posts of a specific user if you know their uid but can't list all posts of the system.
Is there a way to access the requested .where() in the security rules or how can I write such rule or structure my data in this case?
Relevant & credits:
Seemingly, I can make a request on a query's limit, offset, and orderBy. But there's nothing on where. See: #1 & #2.
I copy-pasted much from this question. I don't see how the accepted answer answers the question. It seems like it answers another case where a user is allowed to list some other users' posts. That is not my case; in my case, what's public is public. So, it doesn't answer the main question in my case, it seems.
There's currently no way, using security rules, to check if a field is being used in query. The only thing you can do is verify that a document field is being used as a filter using only values you allow.
Instead, consider duplicating enough data into another collection organized like this:
user-posts (collection)
{uid} (document using UID as document ID)
posts (subcollection)
{postId} (documents using post ID as document ID)
This will require the client to call out a UID to query in order to get all the posts associated with that user. You can store as much information about the post documents as you like, for the purpose of satisfying the query.
Duplicating data like this is common in NoSQL databases. You might even want to make this your new default structure if you don't want your users to query across all posts at any given moment. Note that a collection group query naming the "posts" subcollection would still query across all posts for all users, so you'd have to make sure your security rules are set up so that this is enabled only when you allow it to happen.
Also note that UIDs are typically not hidden from users, especially if your web site is collaborative in nature, and you combine multiple users' data on a single page.

How to list all documents the user can read from a Firestore collection?

How do you get all documents in a collection, for which the current user has read permissions?
Trying to get all documents results in a permissions error, because it includes attempts to read documents where the user does not have permission (rather than returning the filtered list of documents).
Each user in this app can belong to multiple groups. Reads are locked down to the groups that they have been added to.
match groups/{group} {
allow read: if exists(/databases/$(database)/documents/groups/$(group)/users/$(request.auth.uid));
}
Here's how this would look with a hypothetical subcollection-contains-id operator.
firestore()
.collection("groups")
.where("users", "subcollection-contains-id", user.uid);
As a temporary workaround I've moved this logic to a cloud function. Here's a shorthand of how it works.
for (let group of firestore().collection("groups")) {
let user = firestore.doc(`groups/${group.id}/users/${uid}`);
if (user.exists) {
// Send this group id to the client
}
}
How can I keep these concerns together and move this logic to the client side without relaxing the security rules?
You could add owners field in the documents inside a collection
owners: ["uid1", "uid2"]
Then, you could get all the posts with uid by searching with array_contains
ref.where("owners", "array-contains", uid)
In rules, you could add sth like these:
allow read: if request.resource.data.owners.hasAny([request.auth.uid]) == true
allow update: if request.resource.data.owners.hasAny([request.auth.uid]) == true

Resources