Short version
I'm using firestore to build an application where users can create courses, add resources to these courses, and then have other users sign up to their courses which would give them access to all of its resources.
I'm struggling to get the security rules set up so that only the course organiser or course members can access the resources.
My main problem is allowing a user to list all resources within a course they have signed up for, as list requests don't allow queries in the security rules which I need to check if that user is a member of that course.
What is the best approach that would allow hundreds (or potentially thousands) of course members to easily list all of a courses resources, while ensuring it's not possible for non-members to access it?
In the limitations section of the docs it specifically mentions moving the roles into a separate collection for large or complex groups, but doing so will require the security rules performing a query to check access which is not possible for list requests.
Full verison
I'm creating a prototype web application with clients directly accessing firestore. Eliminating the need for a backend to handle simple data access is refreshing, but I'm now really struggling to work out the correct data structure and approach for my use-case, particularly around implementing appropriate security rules.
I've worked extensively with relational databases, but am quite new to nosql databases and firestore specifically which doesn't help.
Concept
The main collections in my application are users, courses, and resources.
Users can create courses within which they create learning resources, other users can then sign up for these courses and therefore have access to all of the resources within them.
As some courses will be invite only it can not be possible for a user to get access to resources for a course they are not a member of.
The main requirements are:
A course organisor needs full read and write access to their courses and resources.
A course member needs full read access to their courses and resources.
Organisors and members both need to be able to easily list all of their courses, and for each course list all of their resources.
Organisors and members should not be able to retrieve information about courses or resources that they do not own or belong to.
My approach
To start with my approach is to heavily utilise sub-collections.
For the organisor perspective users have courses which have resources, and so I modelled it that way.
users -> courses -> resources
Security rules for the organisor were pretty straightforward here too, as the user id is part of the document path it can be easily checked with the user uid available (thanks firebase auth).
i.e
match /users/{userId}/courses/{courseId}/resources/{resourceId} {
allow read, write, update, delete: if (request.auth.uid == userId)
}
Giving members read-only access proved a little bit more difficult though. I needed somewhere appropriate to store that mapping.
I ended up trying to use a sub-collection of users to store that, so course memberships could be looked up with:
/users/{userId}/course_memberships/{courseId}
The theory being I could then write a security rule to grant read access to courses and resources that looked like:
match /users/{userId}/courses/{courseId}/resources/{resourceId} {
allow read: if exists(/databases/$(database)/documents/users/$(request.auth.uid)/course_memberships/${courseId})
}
However this only works when retrieving a single document, list requests won't evaluate a query in the security rules, therefore this set up doesn't allow a course member to view a list of all resources in a given course.
I can't work out what the correct approach here is, the only options I see are:
Create a separate collection hierarchy for each member that contains all of their courses and all of their resources, including key information used for listing their resources, and synchronise these using cloud functions. I understand that nosql is about storing denormalised data, but this feels like a lot of extra data, particularly when it's only necessary for security rules.
Add the uid of every member to an array on every resource (could
be in the thousands for each), which also feels like a lot of extra data and it would
be exposing those ids to all other members which seems wrong.
Is there a better approach here, or is firestore not well suited for this type of problem?
Hopefully this question hasn't been answered already, I did a search and couldn't find something covering quite the same scenario.
A read rule can be broken into get and list, and the latter can apply to queries.
Example:
allow list: if [condition];
More information can be found here.
Related
I am trying to see if Firestore is the right tool for my need.
I am designing a multi-tenanted system using Firestore for scalability and security. Can this be done correctly using just Firestore, Firebase authentication, and Firebase rules to handle all CRUD operations? The idea behind the entities is:
System > Account > Business > Customer
Roles at each entity level would be:
Administrator, Manager, Support, Report, Unauthenticated (the customer)
Accounts have businesses and businesses have customers. Customers would not need to authenticate but read a specific document (designated by the business) and then create their own specific new document. They could create multiple documents but there would be time-based volume thresholds (no DOS!).
I believe Firestore Rules would work great if it didn’t have the Account as a superset that has multiple of the Businesses.
Is it possible to create a system like this with just Firestore, authentication, and rules alone? I want to keep things simple but also not pound a nail using a pair of pliers.
Thanks in advance for the insight!
With a set of well defined Custom Claims you can indeed use Firestore for such a multi-tenant system.
In your security rules, you would combined the different claims, depending on the access role.
In particular you should:
Check the user has the correct claims e.g. auth.token.businessId == "xyz", auth.token.role == "Support"
Check that the user is writing/reading a doc that corresponds to his entity, e.g. allow create: if request.auth.uid == userId && request.resource.data.businessId == auth.token.businessId
Don't forget that Security Rules are not filters, so in each query executed by a user, you need to add the different ids from his/her custom claims (businessId, accountId, etc...). Either by mimicking your entities tree with (sub)collections, or by using some where() clauses in the queries.
Note that since Customers would not need to authenticate, they could potentially read and create documents from/for others businesses, if they have the ID of these businesses (i.e. the DocumentReferences).
You will probably have to manage a set of claims for each user (role, account, business, ... ), which can become complex, so you might be interested by the following article which explains how to create an Admin module for managing Firebase users access and roles.
I am creating a react native application using Firestore and I am not sure how to implement secure schema validation on document creation and update.
If I understand security rules, it is possible to:
Limit who can perform operations (update, read, write, etc.) on documents
Limit operations allowed based on field conditionals
Limit operations allowed based on custom functions (post w/ examples)
My concern is that because of the client side nature of the requests, a savvy user could utilize their authentication and some client side code to .set() any field or map/object to any value they want unless a security rule prevents it. It appears I could use very complicated custom functions to validate the data received. I could also validate every update and create through a Cloud Function API, but I am attempting to use the Firestore database itself whenever possible.
Am I right to be concerned about the potential for users to abuse their .set() field creation abilities on authorized documents (i.e. documents with minimal userId rules)?
Is there an accepted way to create security rules that prevent client abuse of documents that don't have custom functions that validate the schema?
You should always consider malicious users, and how they might affect your data, no matter whether you write the validation in security rules or in more traditional code in Cloud Functions.
Compare these two statements from your question:
"I could use very complicated custom functions to validate the data received"
"I could also validate every update and create through a Cloud Function API"
In both cases you're writing custom code to ensure the data the user enters is valid according to your business rules. Since these rules are specific to your business, there's no way to prevent you having to write them. The only difference is where you write these business rules. With Cloud Functions you're writing the validations in regular JavaScript code, in an environment you may already be familiar with. With security rules you're writing the validations in a domain-specific language, which you'll have to learn.
I personally far prefer writing my business rules into Firestore's server-side security rule language, and then use Cloud Functions for implementing business logic on top of that validated data.
If you are worried that user might just reverse engineer your app and mess up your code to harm your database, then yes this is possible. You should have proper rules set. Talking of updating data in database from app, try to update it through cloud functions as far as possible. This way you might need to give less access to your users to the database directly.
You can check my answer here. This will help you setting rules and some ways to code adapt your app code based on situation. The answer also has some lines on where can one use cloud functions to reduce direct contact with the database.
And if there is no know or you feel the information should be directly updated to the database, change your rules to this: ".write": "$uid === auth.uid" .
Here $uid is name of parent node and can be anything else. This way a user can access his/her data only and even if the user modifies your app, they can harm their data only. (You should have correct rules set).
You can check out this link for most of the rules combinations.
And do check the answer whose link is above. That might clarify how it will secure your database to some extent. If you can provide any particular situation regarding your app and want some information for how to set rules there, feel free to drop a comment :-)
My users can create documents (let's say tasks) in a subcollection with a bunch of security rules checking for authentication, permissions and data validity. They can even select multiple tasks and copy them in the same collection.
Now, a regular user will likely create at most a hundred tasks at once, but what if someone with bad intentions manage to obtain my database credentials, authenticate and try to create a huge number of valid documents programmatically? This will result in Firestore scaling without problems and an unexpected surprise in my Firebase billing.
This is my first concern, but I'm also thinking about the possibility to limit a collection size for other reasons, and it would be at the same time a solution for the problem described.
I read about techniques to count documents in a collection described in the Firestore documentation, but I did not found a solution.
Keeping a counter on a doc field updated with a transaction in a cloud function would be inefficient in my case. Distributed counters increase the complexity of my data model a bit, and also I would not know how to properly read those counters in security rules for every task creation, and even if that would be an efficient solution.
Does anyone has suggestions?
I believe the way for a person to gain read/write access to your database would be to either to hack Google servers, in which case no one is safe and it doesn't really matter what you do, or to guess the exact name of your collections and documents.
As for the latter case, what I have done in my project is that for each collection and document I have used the name I wanted plus random 10-char Strings (including all kinds of chars and numbers. For example Users-x5NfaS1jCb) which kind of serve as independent, separate passwords every step of the way. This, at least, makes it difficult to guess the name of the collections and documents.
(Just like mentioned in the question) If using authentication does not cause any complications for you project, you can use it to further raise the security of your database by limiting access to users authenticating through your app only.
I guess (have never tried it) you can make use of Firebase Functions to limit the number of documents available in any given collection based on the criteria you want. This function will be invoked every time an event in created in the database.
If by "obtain my database credentials", you mean finding the username and password to your Firebase account, well it doesn't really matter what you do again. If they know what they are doing, they can take so many advantages that this particular issue will be the least of your problems.
All in all, if you ask me, your database is safe unless either someone guesses your collection and document names, or gains access to your Firebase account.
These are the only things I can think of for now. I'll try to update my answer later.
I want a group of users to access files stored in Cloud Storage, but I want to make sure they are authorized. Do the unique ids generated by Firestore create enough protection to make them unguessable?
I have my files stored using this structure in Firestore:
/projects/uidOfProject/files/uidOfFile
I made sure that only authorized users can view uidOfProject and uidOfFile using Firestore Rules.
I store that actual files in Storage here:
/projects/uidOfProject/files/uidOfFile
But, I cannot lock down this path to only the authenticated user id, because other users can access this project.
Is the fact that I have two unique ids enough to prevent a user who doesn't have access from finding these files? What are the odds of a user figuring out both the uidOfProject and uidOfFile and manipulating that file? Is there a more secure way of doing this? I know cloud functions could offer a solution, but at a cost of speed.
Do the unique ids generated by Firestore create enough protection to
make them unguessable?
Security through obscurity is NOT security. Good reference to read.
Unguessable, probably. However, due to the somewhat public nature of URLs, logfiles, information leaks, "hey check this out" favors, etc. objects that are not properly protected will be discovered.
If only users of the project can access the files, can they also list the files? If yes, curiosity might take place browsing to see what is there.
I'm new to meteor and I've reading a lot however I'm a little confused around the meteor.users collection and the best way to use it. My interpretation of the best practice guide is that meteor.users collection should only be used for managing the accounts.ui package; email, password and username. The guide states that profile is insecure, a flaw in the original meteor design and should not be used.
So my question is, if I want to create a user profile that contains things like first name, last name, age, address, avatar etc do I create a separate collection like 'userProfile' and link it using the meteor.userid or am I suppose to keep it in the meteor.users collection somehow
Common practice is to put user profile information such as the kind you're describing into Meteor.user().profile. In fact people often do much more, for example memberships in groups, arrays of postIds, all kinds of things. Keeping a separate 1:1 profile collection is an option but there's no fundamental reason to do so that I can think of. On the contrary it makes things just a bit more complicated.
Update: As #jonatan points out in the comments, the Meteor Guide has now unrecommended the use of the profile field in the user document.
Instead they recommend storing custom user information as top-level keys in the user document. This is not only more secure but also more performant since incremental updates can get published over DDP on top-level keys but on sub-keys.
Meteor.user().profile is always auto-published for the current user even after the autopublish package has been removed. Information about other users is not published at all unless you explicitly setup a publication. In that case care must be taken to only publish those fields that should be visible to other users. For example you may only want to publish the usernames of other users and not their email addresses for privacy. You would do this with:
Meteor.publish('otherUsers',function(){
return Meteor.users.find({},{ fields: { 'profile.username': 1 }});
});
You might also restrict the set of other users that is published based on them being connected in some way to the current user to avoid publishing all users all the time.
You should also avoid publishing the services key which contains security information about the user (ex: the bcrypt of their password). As #David Weldon points out in the comments, you shouldn't put other security information in the profile either and you probably want a deny rule on the user modifying their own profile from the client.