How to limit the number of collections created inside a document in firebase? - google-cloud-firestore

In firebase you can create a nested collection that could potentially extend a document, can this limit be set to certain limit with in security rules ?
I have this model where I want to extend the current doc with more sub collections lets say for the sake of the argument I want data-1 and data-2 as sub collections inside the doc , the issue is I want to prevent the creation of more than those 2 docs.
is this possible in security rules ?
allow create:
if getCreateUpdateCollection(userId)
&& request.resource.data.keys().hasOnly(
['data', 'src', 'count'])

You can limit what collections can be created based on their name, by including the names of those collections in the security rules. For example:
service cloud.firestore {
match /databases/{database}/documents {
match /personal-programs/{person} {
allow read, write: if <condition>;
match /data/Routine-1 {
allow read, write: if <condition>;
}
}
}
}
You can't otherwise restricts how many subcollection can be created though, as that'd require the rules engine to check the individual collections which wouldn't scale.
If you really want to restrict on the count of documents/subcollections, you will have to store that count in the parent document, increment/decrement it with every relevant create, and validate that operation in security rules. With those steps out of the way can you then restrict creation of new documents when the count in the parent document has reached a certain maximum.

Related

Firestore Security Rule for allowing only one document

I have a collection that should have only one document or none.
Is it possible to create a security rule to validate this condition?
I want to block an add if there's a document there already. In that case only an update is allowed.
It's not possible to do this with security rules without also adding another document that maintains the count of documents, and adding more security rules to require that its count stay up to date. (It doesn't have to be a count - it could be a boolean - but it will have to be atomically up to date regardless.)
See also: How do you force a Firestore client app to maintain a correct document count for a collection?

Firestore security rules: How to constrain queries that use get(<document_path>)?

So I have a query (that fails). It reads like this: "As I user I can list all the businesses, which I'm a part of, for an organization".
fs
.collection('businesses')
.where('organizationUid', isEqualTo: 'some-organization-id')
.get();
And a security rule to protect it (the gist of it):
match /businesses/{businessId} {
function isStaffOrHigher() {
return get(/databases/$(database)/documents/businesses/$(businessId)/users/$(request.auth.uid)).data.role >= 50;
}
allow read: if isStaffOrHigher();
match /orders/{orderId} {
allow read, write: if isStaffOrHigher();
}
match /users/{userId} {
allow read: if request.auth.uid == userId || isStaffOrHigher();
}
}
Basically it looks up the user's role in his user document (that is owned by that business). This type of rule (that uses the get() operator) works for sub-collections (there's no problem querying orders, for example) of {businessId}, but not for queries that attempts to list the businesses.
Now I KNOW that the organizationUid is a valid constraint, but having read Rules are not filters, I can understand why Firestore can't validate this claim without reading a ton of data.
The question is just, then how do I solve this? And how does Firestore validate the constraint for sub-collections correctly?
Security rules won't do what you want because it would involve reading another document for every document matched by the query. The rules can't know ahead of time what those documents are going to be, as there are variables (businessId) in the path. If this query would yield a millions of documents the businesses collection, you can see how it would be problematic (and expensive for you) to read each of the matching documents from /businesses/$(businessId)/users/$(request.auth.uid) to find out if the entire query should be allowed rejected.
Rules must operate extremely quickly in order to scale in the way that Firestore needs to scale. The limitation that rules can't be filters is part of that requirement for scalability. It's also why there is an overall limit of 10 documents read with get() per rule evaluation.
There is no workaround here from a rules perspective, other than to perform multiple queries, each within bounds of your rules for each collection, and merge the results in your client app.

Firestore! setting limits for documents created per day in 'Rules'

How can I set posts 'Documents' limits that can the user create per day, for example I want to set the limit for 5 posts per day, so is there any rule something like:
match /Post/{id} {
allow create: if ........
&& getAfter(/databases/$(database)/documents/Post/$(id))*LAST 5*".data.createdDate <= 24H....
}
What you're trying to do isn't possible with security rules without some of your own record-keeping. Since security rules don't let you perform queries other than single-document get(), you won't be able to find out what the user has done to a collection by looking at the documents in the collection itself. You will have to maintain some sort of per-user record in a single document with a summary of what they've done over time, then use that known document in a rule that would deny access if they have already exceeded the limits you set. There is nothing very easy or straightforward abut this, and you might be better off requiring the user to go through a backed that enforces the limits instead of using security rules.

Limit fields written on nested object with Firestore security rules

tl;dr: I think Set needs a way to get an element (set.toList()[0]), but maybe I'm missing something!
Hello! I'm working on a budgeting app using Firestore with a large number of small objects (credit card transactions). To limit the number of reads, it doesn't make sense to store each transaction as a separate document since a user is likely to want ~hundreds of transactions at a time.
Instead, I have a container to hold many transactions that looks like this:
/user/{user_id}/transactions/{container_id}
container: {
transactions: {
transaction_id_1: {
amount: 8.25,
note: 'chipotle lunch'
},
transaction_id_2: {
amount: 12.01
}
}
}
This works great, but I don't think the security rules can work for the write. I'd like to allow users to modify some fields (note) but not other fields (amount). If each transaction was a document, we could do this with MapDiff, but the nesting makes it harder.
Since we can't write for loops, if we constrain ourselves to one updated transaction per write, this should be completely possible with nested MapDiffs like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents/{document=**} {
function allowTransactionUpdate() {
let transactionId = <transaction ID of the single transaction being updated>;
// Limit fields updated for container.
return request.resource.data.diff(resource.data).changedKeys()
.hasOnly(['transactions']) &&
// Make sure only one transaction changed.
request.resource.data.transactions.diff(resource.data.transactions)
.changedKeys().hasOnly([transactionId]) &&
// Verify the transaction already exists.
transactionId in resource.data.transactions &&
// Only allow certain fields to be updated on that transaction.
request.resource.data.transactions[transactionId]
.diff(resource.data.transactions[transactionId]).affectedKeys()
.hasOnly(['note']);
}
match /transactions/{transMonthId} {
allow update: if allowTransactionWrite();
}
allow read, write: if false;
}
}
This would work great... if we could use MapDiff to get the transaction that changed in the container.transactions Map:
let transactionId = request.resource.data.transactions
.diff(resource.data.transactions).changedKeys()[0];
The key missing part is the last bit: [0]. Currently, Sets offer no way to get an element, which means that converting something to a Set (and thus anything using MapDiff) is a dead end: You can't ever actually know what the value is in a Set. It can only be compared to other Sets.
Otherwise... am I missing something? Is there another way to be limiting fields on the nested update?
Other options would be:
Using a custom backend to do this write, which is doable but sad since a big advantage of Firestore is minimal backend + enforcement in security rules.
Put user-editable properties in one container document + non-editable properties in another, but that doubles reads and adds annoying complexity to client subscriptions.
Accepting that this is not possible and using a document per-transaction, which will result in 100x more reads. ;)
for anyone else that is looking for an example on nested objects and MapDiff
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function affectedKeys(keys){
return request.resource.data.diff(resource.data).affectedKeys().hasOnly(keys)
}
function affectedKeysObj(obj1Key, obj2Key, keys){
return request.resource.data[obj1Key].diff(resource.data[obj2Key]).affectedKeys().hasOnly(keys)
}
match /{document=**} {
allow read, write: if false;
}
match /users/{uid}{
allow get: if request.auth.uid == uid;
allow update: if request.auth.uid == uid
&& ! affectedKeys(["meta"])
&& affectedKeys(["userData"])
&& affectedKeysObj("userData", "userData", ["bio", "displayName"]);
}
}
}
In this case I wanted the user to be able to edit ["bio", "displayName"] within the userData map, but I also wanted to disallow editing of the meta map.
however pertaining to the question, Doug Stevensons is right, I'm just adding that this is how I use MapDiff with nested objects.
You are not missing something. What you're trying to do is not possible with security rules.
If you intend to collect items of data, and you want to reference those items of data and protect with with security rules, they should be individual documents in a collection or subcollection. Trying to jam them all in a single document is not advisable, nor is it scalable. If you are doing this to save on document reads, you're quickly finding out that this sort of "optimization" is not actually a very helpful one when it comes to security rules and managing those individual items. It's far easier and straightforward to protect items of data as individual documents than it is to manage them in a single document.
If you really must store everything together, I suggest limiting write access via some backend where you can write custom logic, and have your clients invoke the backend whenever they need to perform writes. Bear in mind that this is not scalable, and you can run into the max document size of 1MB, which is a more expensive problem to solve than the one you started out with.

Cannot Query in Firestore Rules Using Claims

In my firestore rules, this allows me to query a list of all of a user's "organizations":
match /organizations/{orgId}{
allow read: if request.auth.uid in resource.data.members;
}
Where members is an array of user id's.
But, if I change this to work with claims:
match /organizations/{orgId}{
allow read: if orgId in request.auth.token.organizations;
}
Where organizations is a list of organization id's.
It seems to work with:
match /organizations/{orgId}{
allow read: if request.auth.token.organizations[orgId] == true;
}
It will let me access the document, but not a list of documents. Is there any way around this?
This doesn't work because security rules are not filters. (Be sure to read the docs in that link.) Also read more here.
When you perform a query on a collection (not a single document get), the filters on the query must absolutely match the requirements of the rules, before the contents of any documents are known. The security rules will not remove individual documents from the results. In this respect, Firestore queries are "all or nothing" - either all of the requested documents are known to match ahead of time, or the entire query fails.
What you have now suggests that each document ID should be read and individually compared to the list of organizations to determine which ones should be returned. Since rules won't do this filtering, it simply simply rejects the query altogether.
What you should probably do instead is simply make one get() for each org ID in the user's claims. It's definitely possible to read custom claims in the client app.