Firebase Cloud Storage Security Rules - Custom Claims are too long, so saving them as strings, but can't extract on Security Rules - firebase-storage

Since some members in our organization have their Custom Claims exceeding 1,000 characters, we started saving the claims as strings (Hopefully that's a right approach).
For example, if a user's groups would have been saved so far to the claims like this:
{groups: ['G1', 'G2', 'G3']} now we save like this {groups: '|1|2|3'}.
And then on the Security Rules, I'm trying to convert in the storage.tmpl.rules the string in to the array.
function hasGroup(group) {
return isSignedIn() && getGroups().hasAny([group]);
// Also tried: group in getGroups()
}
function getGroups() {
// '|1|2|3' => ['', '1', '2', '3'] => '|G|G1|G2|G3' => ['', 'G1', 'G2', 'G3']
return request.auth.token.groups.split('|').join('|G').split('|');
}
match /path/path/{groupId}/path/{allPaths=**} {
allow write: hasGroup(groupId);
}
I keep getting denied! What am I missing? Also, there are no resources on the web on practices to minimize claims size. AND also you can't debug that in the simulator. Thank you for reading, any help would help.

If you need to exceed the size limits of custom claims, the first thing you should consider is not using custom claims. Instead, you can store per-user data in a document identified by the user's UID, and get() that document to check its fields in security rules. Clear examples can be found in the documentation. For example:
// Allow the user to delete cities if their user document has the
// 'admin' field set to 'true'
allow delete: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.admin == true
Before you raise an objection about requiring the cost of a document read, I'll point out that custom claims are inherently limited, and can not scale at all. This is one of the main reasons that security rules allow reading of documents with much larger capacity. When applications need to scale beyond simple use cases, costs are unvoidable.
You should have additional rules that restrict write access to the documents that you use to store per-user permissions, as users should probably not be able to write the documents that determine access to other resources.

Related

How do you know if your firebase security rules are good enough so you can't get hacked?

I am brand new to firebase and i'm not sure if my firebase security rules are good enough so no one can hack into my database and change whatever they want. How do you know if your firebase security rules are any good?
I really do not want to allow anyone to change any data unless they are the current user changing only their personal data.
Here is a picture of my current firebase security rules:
How do I know if my firebase security rules are any good?
Also not sure if this will be useful but I only have 2 collections in my firestore data. The first one is "users" that just has basic information about my users (name, email address, etc). The second is "posts" that just has basic info about a post (likes, comments, etc)
How do you know if your firebase security rules are good enough so you can't get hacked?
That is really unanswerable. It's like asking "How do I know I'm the smartest person in the world"; you can't know that without competing with every person in the world in a battle of the brains.
Your security rules are a bit like that, except that you're not competing with everyone else but only with folks trying to access your data in a malicious way. And instead of an undefined battle of the brains, you're pitching your security rules (which they can't see) against their skills of deduction and familiarity with the API.
Good security rules allow exactly what your code also does, and nothing else.
A good example of this is your first create rule:
match /users/{userID} {
allow create: if true;
}
So this rule allows anyone in the world to create a document under any ID that they want in your database, simply by calling firebase.firestore().document("users/IAMTREV347").set({ whateverKeyIWant: "WithWhateverValue" }).
Your own code is probably be a bit more restrained than that. For example, it seems that you want to store documents in /users/$uid, so under the UID of the current user in your app. In code that might be something like this:
const uid = firebase.auth().currentUser.uid;
firebase.firestore().collection("users").doc(uid).set({
uid: uid,
name: "Trev345"
})
So you'll need to tighten your code to:
Only allow the user to write to the document with their own UID both as the document ID and in the uid field.
Only allow the user to write the name field in that document.
If you modify your rules like that, they allow exactly what the code does and nothing else, so there is no room for anyone to abuse them.
So the proper create rule would be:
match /users/{userID} {
allow create: if request.auth.uid == $userID &&
request.document.data.uid == $userID &&
(request.resource.data.keys().hasOnly(['name', 'uid']));
}
You should go through each of your rules like that and through each piece of code that accesses the database, and give the minimum permission that allows the code to work.

Firestore Rule - limiting "list" access

I have a collection on which I want to provide list access, but only in a limited manner for most users.
All users should be able to do this: (the string valuex can be anything)
collection("XYZ").where("fieldx", "==", "valuex").get()
Only admins can get all the documents:
collection("XYZ").get()
Note that as valuex can be anything, at the end of the day all users can see all documents. The difference is that non-admins need to know what to query, admins don't, they get it all directly.
The only solution I have found is to force non-admins to write to a document the value they are querying, prior to calling get. The rules then are:
allow list: if isadmin() || resource.data.fieldx == getvaluex();
function isadmin() { return request.auth.token.get("admin", false); }
function getvaluex() { return get(/databases/$(database)/documents/users/$(request.auth.uid).data.valuex; }
That way all returned documents must have the same value for fieldx. But this solution 1) needs 1 additional write 2) adds a read in the rules and 3) in my case valuex is sensitive and I dont want the user to have to store it in Firestore.
So is there any better solution?
Is it possible for instance to limit the usage of an index to only some users? (both queries above actually have more where statements and require each a specific composite index).
Is it possible to compare the returned documents between each others to ensure they all have the same value for fieldx?
The way I would do it is this:
Don't allow non-admins to make those direct requests to the database at all.
Instead, have them send a request to a Firebase Http function.
The Http function has admin access to the db, it can accept any valuex non-null value.
It queries the db using that valuex, on behalf of the non-admin users, and returns the results.
This way, you can keep the documents in collection XYZ locked to non-admins in your Firestore Rules.
You can even keep sensitive data in those documents, since you have control on what you share with users. You can control that by choosing which fields your HTTP function will return to clients.
Mind you, Firebase function invocations are way cheaper than making additional writes/reads.
Firestore works well for easy/normalized access from clients to collections and documents.
What you are trying to do is pretty specific to your implementation of the these "lists".
You may create another collection (list_auth) that tracks the accesses to the list.
In the security access you can create a security rule for the collection that looks up the permissions of user into the list by accessing the list_auth collection.
https://firebase.google.com/docs/firestore/security/rules-conditions#access_other_documents

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.