I'm trying to secure requests to a collection to allow any single get, but only to allow list if a specific key is matched.
Database structure is like this:
projects
project1
name: "Project 1 name"
board_id: "board1"
project2
name: "Project 2 name"
board_id: "board2"
boards
board1
board2
The Firestore query I'm making from Vue:
// Only return projects matching the requested board_id
db
.collection("projects")
.where("board_id", "==", this.board_id)
The security rules I'd like to have would be something like this:
match /projects/{project} {
allow get: if true // this works
allow list: if resource.data.board_id == [** the board_id in the query **]
// OR
allow list: if [** the board_id in the query **] != null
I want to do this so you can list the projects in a specific board, but can't just list everything.
Is there a way to access the requested .where() in the security rules or do I need to nest my projects collection inside my boards collection and secure it that way?
It really depends on how you want to query data in the future. If you have no requirement to list all of the projects (irrespective of the board), then your current data model is better and can be secured by adding the allowed boards as a map {board_id: true} or (ideally) sub-collection to the /users document.
Current data model
Database
/projects/{project_id}
/boards/{board_id}
/users/{uid}/boardPermissions/{board_id}
Security rules
match /projects/{project} {
allow list: if exists(/databases/$(database)/documents/users/$(request.auth.uid)/boardPermissions/${resource.data.board_id})
Alternative data model
If you want to totally partition your data (which is what I tend to do for many of my projects), then create the following model
Database
/boards/{board_id}/projects/{project_id}
/users/{uid}/boardPermissions/{board_id}
Security rules
match /boards/{board_id}/projects/{project_id} {
allow list: if exists(/databases/$(database)/documents/users/$(request.auth.uid)/boardPermissions/${board_id})
Related
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.
I'm working on an app with Firestore. I use Firebase Authentication for managing users.
The thing is that most of the data in my app would be user-specific. Meaning that users should only have access to their own data.
I've already done research about Firestore security rules, so I know how to handle the security part.
However, I'm wondering how to properly model user-specific data in general. First piece of data I'm adding are categories, that have (for now) 2 properties: name and type.
After some research, I found it's good to create a collection called categories and then a document per-user, each named after the user's ID (UID).
But then, within such a user-specific document, I want to add my categories. One way I figured it out is like on the screenshot below:
So, to describe this approach in a more generic way:
New collection for each data type (e.g. categories)
Inside the collection, separate document named with UID for each user
In each user-specific document, a map with an object's data (e.g. category_1 map with fields name = groceries and type = expense
However, what worries me here is that I need to somehow invent these names of the maps like category_1, category_2 etc... I have a feeling something is wrong in this model, but my strong SQL background doesn't allow me to think that through 😉
Do you have any ideas whether this model is a good one or what problems could it produce later? Maybe you can suggest a better approach for modeling user-specific data in Firestore database?
Is there any limit on how many categories can a single user have? If not then it'll be better to create a collection for categories to avoid hitting 1 MB max document size. If there is a limit and you decide to use a map, I'd recommend creating a map field categories and them as it's children as shown below so if you add any other fields in the document, it'll be much more categorized:
{
categories: {
category_1: {
name: "",
type: ""
},
category_2: {
name: "",
type: ""
}
}
}
However, creating a sub-collection could be better choice as well if each category gets more data in future and you need some queries on categories.
users -> {userId} -> categories -> {categoryId}
(col) (doc) (col) (doc)
As the categories are in a sub-collection you don't need to add userId field in every category document. However you can also create 2 different root collections namely - users and categories:
users -> {userId}
(col) (doc)
categories -> {categoryId}
(col) (doc)
In this case you would have to store userId field to filter between owner of those categories.
#Alex Mamo has perfectly explained difference between using a root level collection and a sub-collection in this post if you need an in-depth explanation: What are the benefits of using a root collection in Firestore vs. a subcollection?
Security rules will be different in both the cases. If you use sub-collections then you can just read user UID from the wildcard like this:
match /users/{userId}/categories/{categoryId} {
allow read, write: if request.auth != null && request.auth.uid == userId;
}
However, if you use a root level collection, the document ID for each category usually won't have user's UID but the UID will be stored in the document as a field. In that case you would have to read the userId from the document being requested.
match /categories/{categoryId} {
allow read, write: if request.auth != null && request.auth.uid == resource.data.userId;
}
what worries me here is that I need to somehow invent these names of the maps
NoSQL database don't have a schema. You can dynamically add any key as required. Just treat every document as a JSON object.
const newKey = "category_123"
const newValue = {name: "newCatName", type: "expense"}
const userDoc = firebase.firestore().collection("users").doc("userId")
userDoc.update({
[`categories.${newKey}`]: newValue
})
This update operation will create a new child in 'categories' map with the provided values.
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.
In a Firestore security rule, I'm trying to check if the user attempting to create/delete a document in one collection is also the owner of that object, as marked in another collection. Basically, my data looks something like this:
In the users collection, each user has a document like this:
{
name: 'john',
userItems: [
{
id: 'random-id',
...
},
...
],
...
}
In the items collection (which I am writing the rule for), all of the items from all of the users of the platform are there, and have Firestore IDs which correspond to the id keys in the elements of the items list of their owners. So if john created an item with the id random-id, his user document would look like the above, and there would be a new document in the items collection with the Firestore ID of random-id.
What I am trying to achieve is to create a security rule wherein a document in the items collection can only be updated if the user document of the currently authed user, which I can access with get(/databases/$(database)/documents/users/$(request.auth.uid)), has an element in their userItems list which has the id key equal to request.resource.id. If this were normal JS, I'd probably do something like:
match /items/{item} {
allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid))
.data
.userItems
.some(userItem =>
userItem.id === request.resource.id
)
}
However, the Firestore List interface is very lacklustre, and doesn't support fancy operations like .some, or even basic/manual looping (as far as I'm aware). I've been trying to come up with some clever way around this, but even .joining the list and .matching the resulting string with some fancy RegExp wouldn't work, since I'm pretty sure that maps would parse as '[object Object]' instead of stringifying properly.
Is there any way to do this using standard Firestore rules, without reconfiguring my DB structure?
What you're trying to do isn't possible with security rules. You will need to either change that way you represent your data (which I recommend, as a list is probably not the best representation), or add more data to satisfy your requirements.
If the the random-id is unique in the list, you should consider using a map instead of a list to represent it, so that you can do simple lookups on the Map that becomes available in rules. If your userItems field was a map indexed by that ID, you could instead say:
allow write: if get(...).data.userItems.keys().hasAny([request.resource.id]);
If for some reason you can't change the field, you will need to duplicate the IDs into a new list field and check it like this:
allow write: if get(...).data.userItemIds.hasAny([request.resource.id]);
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.