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
Related
I'm currently working on an application where users can create groups and invite others in it.
I would like people in the same group to be able to see their first and last names.
To do that, I have a collection named Users where each of the users have a document contains all their personnal data, like first and last names, phone, position , ...
I have also another collection named Groups, where all of my groups are stored, with their name, and an array contaning the ID of the members.
When an user open the app, a first request is done for request his groups (he recieve the groups names and the arrays of members). After, if he want to know the user in a certain group, another request is done for search only the first and last name of all the members.
So, I imagine that there is a query that will return me only the fields that I would like to retrieve, and that there is a rule allowing a potential hacker to be refused access to the entire user document except if the user is the owner of the document.
// For retrieving my user's groups
Stream<List<Group>?> get organizations {
return firestore
.collection('Groups')
.where('members', arrayContains: this.uid)
.snapshots()
.map(_groupsFromSnapshot);
}
// For retrieving names of the members of a group
Stream<List<Member>?> getMembers(Group group){
return firestore
.collection('Users')
// and i dont know what to do here ...
}
With the Client SDKs and the Flutter plugin it is not possible to get only a subset of the fields of a Document. When you fetch a Document you get it with all its fields.
If you want to get only a subset of the fields of a document, you can implements the two following approaches:
Denormalize your data: You create another collection which contains documents that only contain the fields you want to expose. You need to synchronize the two collections (the Users collection, which is the "master", and the new collection): for that it's quite common to use a Cloud Function. Note also that it's a good idea to use the same documentID for the linked documents in the two collections.
Use the Firestore REST API to fetch the data: With the REST API you can use a DocumentMask when you fetch one document with the get method or a Projection when you query a Collection. The DocumentMask or the Projection will "restrict a get operation on a document to a subset of its fields". You can use the http package for calling the API from your Flutter app.
HOWEVER, the second approach is not valid if you want to protect the other users data: a malicious user could call the Firestore REST API with the same request but without a DocumentMask or a Projection. In other words, this approach is interesting if you just want to minimize the network traffic, not if you want to keep secret certain fields of a document.
So, for your specific use case, you need to go for the first solution.
Several questions address whether knowing a Firestore uid allows hackers to edit that person's data, like this question and this question. My question is about security rules to filter when users can read another's data.
Specifically, I have a social media app that allows people to post data anonymously. My data model is /users/{user}/posts/{post}. I use db.collectionGroup("posts") to build a timeline of posts, (some anonymous, others with users' names).
Posts that are not anonymous have a valid uid, so it wouldn't be tough for a hacker to figure out someone's uid, which I'm not concerned about. My concern is whether a hacker could then query usersRef.document(uid).posts.getDocuments(); to get all the posts of that user, including the anonymous ones?
Because my app builds timelines from users "posts" collection, I can't write a rule that they can't read another user's posts. Can I write a rule that they can only read posts with collectionGroup?
That's not going to be possible with the way things are structured now. Here's the way you write a rule to allow collection group queries, as described in the documentation
match /{path=**}/posts/{post} {
allow read: if ...condition...;
}
The path wildcard in the rule explicitly allows all reads for all collections named "posts". The rule does not limit the reads to only collection group queries - any normal collection query on any "posts" will be allowed.
Bear in mind also that a collection group query would not hide any data from the caller compared to a normal collection query. The query results will still contain a reference to the full path of each document, which includes the document uid in the 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.
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.
Right now, in my Firestore rules only registered users have read / write access to my project. However, I also want to verify in my registration process if an E-Mail already exists, which means also anonymous user have access to my "users" data.
I am unsure how this is compliant from a security perspective. Should I create something like a "emails" collection and duplicate all E-Mail addresses here and allow anonymous users to query this?
Yup, that is indeed the typical approach. Duplicate the data that you want anonymously accessible into a separate collection, and grant broader access restrictions there.
In the rules for that collection of email addresses you will typically use granular rules that allow reading each specific document, but to disallow listing all documents. Something like:
match /emails/{email} {
// Applies to single document read requests
allow get: if <condition>;
// Applies to queries and collection read requests
allow list: if false;
}
This means that once the user has typed a specific email address, you can check if a document exists for that email address. But they can't just get all documents from the collection to scrape your user base.