I am building web platform where user can upload photos and videos, I use firebase storage for it, but I come a cross one problem that I cant solve.
I want to manually check all of the files before making them public.
So my first idea was to make to separate folder, and when admin account approve the file, he can move it from /private/img.jpg to /public/img.jpg. But for some reason, firebase storage doesn't have function to move files.
So I come with new idea to use custom metadata, to block to read images that doesn't have metadata visibility set to 'public'. But for me, this solution doesn't seems to be the best... and more you aren't able to block listing file with specific metadata, so even when the file is private, everybody can see that the file exist, and the name, and this is potential risk for me.
So are there anybody, who can help find new solution?
Thank you :)
Approach #1:
You can use a Callable Cloud Function that you call from your app and which:
Checks that the user who is calling has the Admin role (use a specific Custom Claim);
If the user has the correct role, copies the file from the private "folder" to the public one (BTW see this answer on folders) by using the copy() method from the Google Cloud Storage Node.js SDK (which is the SDK to be used in a Cloud Function).
Approach #2:
Another totally different approach would be to take advantage of the new Cloud Storage Security Rules functionality which allows "building flexible permissions to protect access to storage files based on data within Firestore".
With this functionality, you would be able to manage access to the files through Documents in Firestore. For each file in Cloud Storage, create a document in Firestore which has the same name as the file and contains a boolean field named (for example) published. By default, it is false and if the Admin approves the file you change it to true.
The security rule would be along the following lines (untested):
service firebase.storage {
match /b/{bucket}/o {
match /.../{fileId} {
allow read: if
firestore.get(/databases/(default)/documents/files/$(fileId)).published
}
}
}
Don't forget to declare the correct Security Rules for the Firestore collection, in such a way only Admin users can approve files.
Related
I'm using Firestore to store my data. This includes user profile details and their current location in documents, which are stored in a collection with the below security rules:
match /profile/{w9o3948s} {
allow read, write: if request.auth.uid != null;
}
Is there any way for people to "browse" the list of documents in the collection and look through user's locations? Or can that only be done by code within in my app?
The document ID is randomly generated, so even if someone hypothetically knows a current document ID - how would they query the document?
Is there any way for people to "browse" the list of documents in the
collection and look through user's locations?
Yes, with your security rules it is totally possible.
As soon as (1) someone has the apiKey of your Firebase Project and (2) the email/password sign-in method is enabled, this person can use the Firebase Auth REST API and sign-up to your project (i.e. create a new account).
Getting the apiKey is not very difficult if you deploy an app linked to your Firebase project (Android, iOS, Web...).
One standard way to give access to only a set of users (for example, the employees of your company, or some paying subscribers to your app) is to use Custom Claims. You will find in the documentation the guidelines for setting access control with Claims.
You may be interested by this article which presents how to build, with a Callable Cloud Function, a module for allowing end-users with a specific Admin role creating other users and how to restrict access to users with one or more specific Custom Claim(s). (disclaimer, I'm the author).
Or can that (i.e. browse the list of documents in the collection) only
be done by code within in my app?
Anybody who can reverse engineer your app can find the name of your Firestore collections and, with an account created as explained above, can access the documents in those collections.
The document ID is randomly generated, so even if someone
hypothetically knows a current document ID - how would they query the
document?
As you will read in the section of the Security Rules documentation dedicated to Granular Operations, using read allows users to get one document AND to list all documents of a Collection (of a Query). So if you want to restrict the read access rights of a user to only his/her profile, you will need to have two different rules for get and list.
So, in conclusion:
Use Custom Claims: they can only be set from a "privileged server environment" through the Firebase Admin SDK. "Privileged server environment” meaning a server that you fully control or a Cloud Function in your Firebase Project.
Fine tune your Security Rules to (a) use the custom claims in the rules and (b) use granular access rights.
I know there are several questions regarding this (e.g. https://stackoverflow.com/a/52808572/3481904), but I still don't have a good solution for my case.
My application has Groups, which are created/removed dynamically, and members (users) can be added/removed at anytime.
Each Group has 0..N private files (Firebase Storage), saved in different paths (all having the prefix groups/{groupId}/...).
In Firestore Security Rules, I use get() & exists() to know if the signed-in-user is part of a group. But I cannot do this in the Firebase Storage Security Rules.
The 2 proposed solution are:
User Claims:
but the token needs to be refreshed (signing out/in, or renewing expired token) which is not acceptable for my use case, because users need to have access immediately once invited. Also, a user can be part of many groups, which can potentially grow over 1000 bytes.
File Metadata:
but Groups can have N files in different paths, so I will need to loop-list all files of a group, and set the userIds of the group-members in the metadata of each file, allowing access to it. This would be an action triggered by Firestore (a Firebase Function), when a member is added/removed.
I don't like this approach because:
needs to loop-list N files and set metadata for each one (not very performant)
To add new files, I think I would need to set create to public (as there is no metadata to check against yet), and then a Function would need to be triggered to add the userIds to the metadata
there might be some seconds of delay to give files access, which could cause problems in my case if the user opens the group page before that time, having a bad experience
So, my questions are:
Is there a better way?
If I only allow the client to get and create all files when authenticated (disallowing delete and list), would this be enough for security? I think that there might be a chance that malicious hackers can upload anything with an anonymous user, or potentially read all private group files if they know the path...
Thanks!
If custom claims don't work for you, there is really no "good" way to implement this. Your only real options are:
Make use of Cloud Functions in some way to mirror the relevant data from Firestore into Storage, placing Firestore document data into Storage object metadata to be checked by rules.
Route all access to Storage through a backend you control (could also be Cloud Functions) that performs all the relevant security checks. If you use Cloud Functions, this will not work for files whose content is greater than 10MB, as that's the limit for the size of the request and response with Cloud Functions.
Please file a feature request with Firebase support to be allow use of Firestore documents in Storage rules - it's a common request. https://support.google.com/firebase/contact/support
I had similar use case, here’s another way to go about it without using file metadata.
Create a private bucket
Upload files to this bucket via cloud function
2a. validate group stuff here then upload to above bucket.
2b. Generate a signed url for uploaded file
2c. Put this signed URL in Firestore where only the group members can read it (eg. /groups/id/urls)
In UI get the signed URL from firestore for given image id in a group and render the image.
Because we generate the signed URL and upload file together there will be no delay in using the image. (The upload might take longer but we can show spinner)
Also we generate the URL once so not incurring any B class operations or extra functions running every time we add new members to groups.
If you want to be more secure you could set expiry of signed urls quite short and rotate them periodically.
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.
Our current Firestore structure is as follows:
Currently we are not using any subcollections
Users have list of companies to which they belong
Every project is connected only with 1 company
Project belongs to a company, when in companyId field is written that company UID
My 1st question is how we can specify security rules defined by this database? Is there some best practice approach?
Our first idea was to do this:
match /databases/{database}/documents/projects/{projectUid}/{document=**} {
allow read: if
(/databases/$(database)/documents/projects/$(projectUid)/companyId) ===
(/databases/$(database)/documents/users/$(request.auth.uid)/companyId)
}
But according to the documentation this would mean that we would have for each read basically 3 reads (2 queries for security and 1 real read from DB). This seems like a waste of queries.
Is there a better approach than this?
We were thinking about changing to subcollections:
at the end we would have in root collections 'companies' and 'users' (to store all users details)
projects would be subcollection of companies
pages would be subcollection of projects
...etc
and companies would contain list of users (not the other way around like now) - but only list, not user details
This way we can use similar approach as from the doc, where each match would contain {companyId} and in allow statement we would use something like
match /databases/{database}/documents/companies/{companyId}/projects/{projectId} {
allow read: if
exists(/databases/$(database)/documents/companies/$(companyId)/users/$(request.auth.uid));
}
Thanks for any recommendations on how to build it in the most scalable and especially most secure way.
Have you considered adding a user's company ID as a custom claim to their profile? That way no additional reads are needed in your security rules.
Since setting these claims requires the Admin SDK, it will require that you can run trusted code somewhere. But if you don't have your own trusted environment yet, you could use Cloud Functions for that e.g. based on some other action like writes to your current Firestore structure.
Adding an answer to Frank.
Borrowing from other API SDKs such as microsoft graph, typically to make a resource request you start by initializing a Client object with an authentication token representing the scope/rights of the user. For example:
const client = new SDKClient(my_auth_token);
The client constructor would have a token validation step on claims. You can then make REST calls such as
const response = await client.someEndpoint({ method: 'POST', body: my_object });
I suggest rather than using the admin SDK for read/write to your firestore, you use the regular firebase nodejs client. To restrict access with security rules, pass a firebase JWT token into this custom SDKClient class with the token that you obtain from the header of your requests. In the constructor, initialize a new firebase 'app'. Because a regular firebase client is
subject to security rules, this will do what you're looking for.
Some example code has already been offered in this answer.
I should add that according to this firebase doc there is a 'warning' to use the admin-sdk server-side, but I'm not sure I see why.
One approach I've thought of for something similar that we are working on, that is, private chatrooms where only certain users have access, is to encrypt all messages with an on-server key, and only grant read access for that key to certain users. That way the extra read only has to occur one time, just when getting the key for the first time, then normal reads with no additional security rules are fine, as an attacker wouldn't be able to do anything with them since they are encrypted and they don't have access to the key.
In firebase, storage feature is comfortable. But I have two questions to use storage feature.
How to prevent users' unlimited file upload? e.g) if I can write /{uid}/{timestamp}/{fileName} path, user can upload till the storage is exploded.
How to trace legacy files and remove them? If a user upload chat message with image file and remove the image, file still exists in the storage. Is there any hook to manage this?
Thank you!
Good questions:
You can use Firebase Storage Security Rules to enforce arbitrary constraints for uploads. As long as the file has a piece of unique identifying information in the name or the metadata, you can tie that to a user and enforce time based access (among other things). A quick example:
match /files/{userId}/{fileName} {
// Only allow the authorized user to write to their files
allow write: if request.auth.uid == userId;
// Allow reads if the resource was created less than an hour ago
allow read: if resource.timeCreated < request.time + duration.value(60, "m");
}
You're asking for a garbage collection system :) The short answer is that it's best if you perform any file deletes along side any database object deletes on the client, but as we all know clients are unreliable. You can also set up a batch job (say every day) that reads the database data from a private backup, checks which blobs are no longer referenced, and deletes them. You can also wait for Google Cloud Functions, and perform actions (such as writing to the database on file upload) to sync this information. Lastly, you can use Object Lifecycle Management methods to clean up objects (delete, archive, etc.) in your bucket after a certain period of time.