CONTEXT:
Suppose there are a products and orders collections on Firestore.
And an order has many products, that is (pseudo-schema):
products {
name: string
}
orders {
items: [{
product_id: (ref products)
quantity: number
}]
}
With these security rules:
match /products/{document=**} {
allow read, write: if request.auth != null;
}
match /orders/{document=**} {
allow read, write: if request.auth != null;
}
SCENARIO:
Now, suppose we have created the product A.
Then, we created the order 1 for the product A.
Next, suppose we delete the product A (which is already being used in the order 1) from the products collection.
This would let the order 1 still referencing the deleted product A.
QUESTION:
Is there a way of writing a Security Rule that prevents the deletion of products that are being used on orders?
First of all, if the goal is just consistency, you can use Functions to delete the references to A so there is no broken linkage.
However, if you specifically want to prevent deletion while linked, you'll need a different strategy, as there is no way to query another path within security rules at present.
Denormalize: keep a list of references in the product
Something like this in your data:
"{product_id}": {
"name": "...",
"orders": {
"order_id": true
}
}
Would allow you to write rules like this:
function isAuthenticated() {
return auth != null;
}
function hasLinks(resource) {
return !resource.data.links;
}
match /products/{pid} {
allow delete: if isAuthenticated() && !hasLinks(request.resource);
}
Other ideas
Using Functions: My first instinct was a queue approach (you queue the delete to the server and the serve decides if there is a link to the product and either deletes it or rejects the request). But that wouldn't work with your current structure either since you can't query across subcollections to find references to the product in each order subcollection. You'd still need a denormalized list of orders to products to use for this (making this pretty much the same as my solution above).
Archive items instead of deleting them: There's probably not a strong need to actually delete items, so archiving them instead could avoid the whole problem set.
Related
First, sorry for my terrible English, it is not my native language...
I am building a simple app in Firebase, using the Firestore database. In my app, users are members of small groups. They have access to other users' data.
In order not to query too many documents (one per user, in a subcollection of the group's document), I have chosen to add the users' data in an array inside the group's document.
Here is my group's document:
{
"name":"fefefefe",
"days":[false,false,false,false,true],
"members":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
}
How can I check with the security rules if a user is in a group ?
Should I use an object instead ?
I'd really prefer not use a subcollection for users, because I would reach the free quota's limits too quickly...
Thank you for your time !
EDIT:
Thanks for the answer. I will change it to an object :
"Members": { uid1 : {}, uid2 : {} }
In general, you need to write a rule like the following:
service cloud.firestore {
match /databases/{database}/documents {
match /collection/{documentId} {
// works if `members` = [uid1, uid2, uid3]
// no way to iterate over a collection and check members
allow read: if request.auth.uid in resource.data.members;
// you could also have `members` = {uid1: {}, uid2: {}}
allow read: if resource.data.members[request.auth.uid] != null;
}
}
}
You could also use subcollections:
service cloud.firestore {
match /databases/{database}/documents {
// Allow a user to read a message if the user is in the room
match /rooms/{roomId} {
match /documents/{documentId} {
allow read: if exists(/databases/$(database)/documents/documents/$(documentId)/users/$(request.auth.uid));
}
match /users/{userId} {
// rules to allow users to operate on a document
}
}
}
}
I made it happen with this code
Allow some user to read/write some document of a collection if this same user is present into an array of another collection
service cloud.firestore {
match /databases/{database}/documents {
match /repositories/{accountId} {
allow read, write: if request.auth.uid in get(/databases/$(database)/documents/accounts/$(accountId)).data.users
}
}
}
Just offering an alternative solution. In my case I store two separate fields. In your case it would be:
"membersSummary":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
"members": ["aaaaaaaa", "bbbbbbbb"]
I'm aware that this is not necessarily optimal but as we're using firebase I assume we're ok with using denormalised data in our documents.
I'd use the members field for collection queries and firestore rules (allow read: if request.auth.uid in resource.data.members; as per Mike's answer above), and the membersSummary for rendering the info in the UI or using the additional fields for other types of processing.
If you use uids as keys then if you wanted to query a collection and list all the documents for which that user is a member, and order them by name, then firebase would need a separate composite index for each uid, which unless you have a fixed set of users (highly unlikely) would basically result in your app breaking.
I really don't like the idea of extra document reads just for access control but if you prefer that approach to tracking two separate related fields then do that. There's no perfect solution - just offering another possibility with its own pros and cons.
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.
I am having an issue with Firestore rules when the permission is stored in another document in another collection. I haven't been able to find any examples of this, but I have read that it can be done.
I want to do it this way to avoid having to do a lot of writes when a student shares his homework list with many other students. Yes, I know this counts as another read.
I have 3 collections, users, permissions, and homework along with some sample data.
users
{
id: fK3ddutEpD2qQqRMXNW,
name: "Steven Smith"
},
{
id: YI2Fx656kkkk25,
name: "Becky Kinsley"
},
{
id: CAFDSDFR244bb,
name: "Tonya Benz"
}
permissions
{
id: fK3ddutEpD2qQqRMXNW,
followers: [YI2Fx656kkkk25,CAFDSDFR244bb]
}
homework
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false
}
The start of my firestore rules:
service cloud.firestore {
//lock down the entire firestore then open rules up.
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /homework/{ } {
allow get: if isSignedIn()
}
// helper functions
function isSignedIn() {
return request.auth != null;
}
function isUser(userId) {
return request.auth.uid == userId;
}
function isOwner(userId) {
return request.auth.uid == resource.data.uid;
}
}
}
Use case:
Steven Smith Shared his homework list with Tonya Benz.
Tonya Benz is logged into the app to view her friend Steven's homework. The app runs this query to get the homework list of Steven Smith.
var homeworkRef = db.collection("homework");
var query = homeworkRef.where("owner", "==", "fK3ddutEpD2qQqRMXNW");
Question:
What is the proper Firestore match rule that takes the "owner" field from the homework collection to look up it up as the id in the permissions collection when the user Tonya Benz is signed in so this query can run.
With your current query and database structure, you won't be able to achieve your goal using security rules.
Firstly, it sounds like you're expecting to be able to filter the results of the query based on the contents of another document. Security rules can't act as query filters. All the documents matched by the query must be granted read access by security rules, or the entire query is denied. You will need to come up with a query that is specific about which documents should be allowed access. Unfortunately, there is no single query that can do this with your current structure, because that would require a sort of "join" between permissions and homework. But Firestore (like all NoSQL databases), do not support joins.
You will need to model your data in such a way that is compatible with rules. You have one option that I can think of.
You could store the list users who should have read have access to a particular document in homework, within that same document, represented as a list field. The query could specify a filter based on the user's uid presence in that list field. And the rule could specify that read access only be granted to users whose IDs are present in that list.
{
id: adfsajkfsk4444,
owner: fK3ddutEpD2qQqRMXNW,
name: "Math Homework",
isDone: false,
readers: [ 'list', 'of', 'userids' ] // filter against this list field
}
The bottom line here is that you'll need to satisfy these two requirements:
Your query needs to be specific about exactly which documents that it expects to be readable. You can't use a rule to filter the results.
Your rule needs a way to determine, using nothing more complicated than the fields of the document itself, or a get() on other known documents, what the access should be for the current uid.
I am a bit stuck here as there is no way to debug those rules. I'd appreciate help with below rules.
I want to access:
/modules/module-id/sessions/session-id/parts/
The comparison with null in the first part of hasCompletedPrerequisiteSession() works well, the second part doesn't!
The path /modules/moduleId/sessions/sessionId/prerequisite points to a reference field.
service cloud.firestore {
match /databases/{database}/documents {
function hasCompletedPrerequisiteSession(moduleId,sessionId) {
// this part works well
return getPrerequisiteSession(moduleId,sessionId) == null ||
// !!! this part does not work !!!
hasCompleted(getPrerequisiteSession(moduleId,sessionId).id);
}
function getPrerequisiteSession(moduleId,sessionId) {
return get(/databases/$(database)/documents/modules/$(moduleId)/sessions/$(sessionId)).data.prerequisite;
}
function hasCompleted(sessionId) {
return exists(/databases/$(database)/documents/progress/$(request.auth.uid)/sessions/$(sessionId));
}
match /modules/{moduleId}/sessions/{sessionId}/parts/{partId} {
allow read: if hasCompletedPrerequisiteSession(moduleId,sessionId);
}
}
}
(If I store the session ID as a string instead of a reference to the session, it works fine.)
Edit
Questions
Reference field in security rules. Assuming modules/moduleId/owner points to a field of the type reference. What is the proper way to get the id of the referenced document?get(../modules/moduleId).data.owner.data.id or get(../modules/moduleId).data.owner or something else?
From Firebase support:
It seems that in your use case, you want to get the document name (sessionId) from the value of your reference field (prerequisite), unfortunately, this is not currently supported by Firestore security rules. I would suggest that you store only the sessionId as String on your prerequisite field, or you can also add String field for the sessionId. Keep in mind that the exists() and get() functions only allow you to check if a document exists, or retrieve the document at the given path.
It might be that around getPrerequisiteSession, after using get to pull the object by ref path, you had to use .data first before referencing the id field. Of course, id field needs to be stored as an object field.
For example, in my case I needed to allow 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:
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();
}
First, sorry for my terrible English, it is not my native language...
I am building a simple app in Firebase, using the Firestore database. In my app, users are members of small groups. They have access to other users' data.
In order not to query too many documents (one per user, in a subcollection of the group's document), I have chosen to add the users' data in an array inside the group's document.
Here is my group's document:
{
"name":"fefefefe",
"days":[false,false,false,false,true],
"members":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
}
How can I check with the security rules if a user is in a group ?
Should I use an object instead ?
I'd really prefer not use a subcollection for users, because I would reach the free quota's limits too quickly...
Thank you for your time !
EDIT:
Thanks for the answer. I will change it to an object :
"Members": { uid1 : {}, uid2 : {} }
In general, you need to write a rule like the following:
service cloud.firestore {
match /databases/{database}/documents {
match /collection/{documentId} {
// works if `members` = [uid1, uid2, uid3]
// no way to iterate over a collection and check members
allow read: if request.auth.uid in resource.data.members;
// you could also have `members` = {uid1: {}, uid2: {}}
allow read: if resource.data.members[request.auth.uid] != null;
}
}
}
You could also use subcollections:
service cloud.firestore {
match /databases/{database}/documents {
// Allow a user to read a message if the user is in the room
match /rooms/{roomId} {
match /documents/{documentId} {
allow read: if exists(/databases/$(database)/documents/documents/$(documentId)/users/$(request.auth.uid));
}
match /users/{userId} {
// rules to allow users to operate on a document
}
}
}
}
I made it happen with this code
Allow some user to read/write some document of a collection if this same user is present into an array of another collection
service cloud.firestore {
match /databases/{database}/documents {
match /repositories/{accountId} {
allow read, write: if request.auth.uid in get(/databases/$(database)/documents/accounts/$(accountId)).data.users
}
}
}
Just offering an alternative solution. In my case I store two separate fields. In your case it would be:
"membersSummary":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
"members": ["aaaaaaaa", "bbbbbbbb"]
I'm aware that this is not necessarily optimal but as we're using firebase I assume we're ok with using denormalised data in our documents.
I'd use the members field for collection queries and firestore rules (allow read: if request.auth.uid in resource.data.members; as per Mike's answer above), and the membersSummary for rendering the info in the UI or using the additional fields for other types of processing.
If you use uids as keys then if you wanted to query a collection and list all the documents for which that user is a member, and order them by name, then firebase would need a separate composite index for each uid, which unless you have a fixed set of users (highly unlikely) would basically result in your app breaking.
I really don't like the idea of extra document reads just for access control but if you prefer that approach to tracking two separate related fields then do that. There's no perfect solution - just offering another possibility with its own pros and cons.