Firestore - security rules for users within companies - firebase

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.

Related

Is it possible to shorten firebase auth UIDs?

I've started designing a referral system and wanted to use firebase auth UIDs as the referral code for each user, however they're longer than I'd like and also not guaranteed to stay small. I've looked into some libraries for short text compression, hashing, etc; But none seem to satisfy my needs. I've recently came across the lovely short-uuid pkg on npm but unfortunately it doesn't seem it works with firebase UIDs(because those aren't UUIDs) but i've been looking for a possible solution that doesn't involve keeping a lookup table of custom IDs to UIDs.
So the real question: is there any good way to compress a short string programmatically and then decompress?
There is no way for you to control the UIDs that Firebase Authentication generates in its various providers.
If you want to use a shorter/friendlier scheme for identifying your users, some options are:
You can generate a shorter ID yourself, and maintain a mapping of those IDs to the ones that Firebase generates. You'll typically want to include a uniqueness check for your shorter IDs, as the chances of collisions rapidly go up for the shorter strings.
A common example of a friendlier identifier for users is to allow users to pick a unique username. This is essentially a variant of the first option, but now with a user-selected ID. Here too you will need to perform a check to prevent duplicates.
You can also creating a custom provider that plugs into Firebase Authentication. When doing this, you control the UID that Firebase uses too. Here you are responsible that the UIDs your provider generates are unique not just within your own provider, but across all providers you've enabled for your project.
As #FrankVanPuffelen explained there is no way for you to control the UIDs that Firebase Authentication automatically generates.
But with the createUser() method of the Admin SDK you can define which UID you want to assign to a new user. As explained in the doc, "if you instead want to specify your own UID for the new user, you can include it as an argument passed to the user creation method"
await admin.auth().createUser(
{
uid: 'some-uid',
email: 'user#example.com',
password: '....',
}
);
You can run this code in a Cloud Function or on a server you own.
Of course, you need to ensure that the user's UID is unique for each user.

how to maintain read counts of documents in firebase

Firebase Firestore: How to monitor read document count by collection?
So first of something similar like question was already asked almost a year ago so dont mark it duplicate cause I need some suggestions in detail.
So here is the scenario,
lets say I have some collections of database and in future I might need to perform some ML on the DB. According to the documents visit.
That is how many times a specific document is visited and for how much time.
I know the mentioned solution above indirectly suggests to perform a read followed by write operation to the database to append the read count every time I visit the database. But it seems this needs to be done from client side
Now if you see, lets say I have some documents and client is only allowed to read the document and not given with access for writing or updating. In this case either I will have to maintain a separate collection specifically to maintain the count, which is of course from client side or else I will have to expose a specific field in the parent document (actual documents from where I am showing the data to clients) to be write enabled and rest remaining field protected.
But fecthing this data from client side sounds an alarm for lot of things and parameters cause I want to collect this data even if the client is not authenticated.
I saw the documentation of cloud functions and it seems there is not trigger function which works as a watch dog for listening if the document is being fetched.
So I want some suggestions on how can we perform this in GCP by creating own custom trigger or hook in a server.
Just a head start will be so usefull.
You cannot keep track of read counts if you are using the Client SDKs. You would have to fetch data from Firestore with some secure env in the middle (Cloud Functions or your own server).
A callable function like this may be useful:
// Returns data for the path passed in data obj
exports.getData = functions.https.onCall(async (data, context) => {
const snapshot = admin.firestore().doc(data.path).get()
//Increment the read count
await admin.firestore().collection("uesrs").doc(context.auth.uid).update({
reads: admin.firestore.FieldValue.increment(1)
})
return snapshot.data()
});
Do note that you are using Firebase Admin SDK in this case which has complete access to all Firebase resources (bypasses all security rules). So you'll need to authorize the user yourself. You can get UID of user calling the function like this: context.auth.uid and then maybe some simple if-else logic will help.
One solution would be to use a Cloud Function in order to read or write from/to Firestore instead of directly interacting with Firestore from you front-end (with one of the Client SDKs).
This way you can keep one or more counters of the number of reads as well as calculate and apply specific access rights and everything is done in the back-end, not in the front-end. With a Callable Cloud Function you can get the user ID of authenticated users out of the box.
Note that by going through a Cloud Function you will loose some advantages of the Client SDKs, for example the ability to use a listener for real-time updates or the possibility to declare access rights through standard security rules. The following article covers the advantages and drawbacks of such approach.

How can any user write to my database securely in Firebase without Auth?

So if I have an E-commerce App that doesn't require Login/Auth, and my users buy in the store, then after verifying the transaction the backend writes the order in the database.
The thing is that my Realtime Database just relies on Stripe transaction key (i.e, someone has paid for an item) to be able to write on the DB, because my rules are set so anyone can write, otherwise I would need every user to log in, but that's not what I want.
Firebase recently notified me that my rules are weak.
How can a make sure my users are able to write to my database in a secure way for my app, without log in/Auth?
There are many different security models you can use with Firebase, and it's important to understand the level of security each provides.
One thing to look into is anonymous auth which lets you "authenticate" a user without actually requiring them to provide any credentials. This provides a way to guarantee that the same device is being used between multiple reads/writes.
In your specific case, it sounds like you might be looking to rely on unguessable tokens. This can be a valid security model for some use cases so long as the key is sufficiently complex as to be unguessable.
At its most basic, the way you'd structure security rules for unguessable URLs is something like:
{
"rules": {
"transactions": {
"$key": {
".read": true,
".write": true
}
}
}
}
This allows users to read/write specific nodes at e.g. transactions/abc123xyzunguessable but importantly does not allow reading/writing to the parent transactions node. Security comes from the fact that only the person who originally got the unguessable token will be able to provide it again in the future.
A better implementation would gate writing on the $key matching the expected unguessable format, adding validation and other read/write rules to ensure that the data is formatted appropriately, and probably also prevent modification of key fields.
These are just some pointers but should help you on your way. The important thing is to make sure that you never leave important information in a place where it can be read through easily guessable URLs.
There is no "secure" way to allow writes to Realtime Database without Firebase Authentication. Without Firebase Auth, either there is full public access, or there is no public access at all.
If you can't use Firebase Auth, what you will need to do instead is make your security rules disallow all direct access to the database from client applications, then create backend APIs to manage access to the database. Your backend APIs will need to somehow validate that the person making the request should have the ability to make the required changes. Then, it will have to use the Firebase Admin SDK to commit those changes to the database.

Is possible to get userID or currentUser subcollection in Firestore with Cloud Functions? [duplicate]

I'm trying to get the UID of the user authenticated by firebase web sdk, in the cloud function. The cloud function is triggered by onWrite event of cloud firestore.
This function is triggered when the logged in user is creating/updating items to the cafe. The authentication is handled by Firebase Auth. The security rules enable write only for logged in users. So this event could be tied to a user.
export const cfun = functions.firestore.document('cafes/{cafeId}/items/{itemId}').onWrite(async event => {
// trying to get the uid here
})
There are examples in the docs that deals with the userId, but in all those cases the userId is part of the document path. But in this model the user is not part of the path, as a cafe could have multiple owners and so could be manipulated by many users. So adding userId to the path is not an option.
It looks like a common case for serverless architecture.
#
Update: Functions triggered by firestore doesn't have event.auth populated. Looking for suggestions on modelling the following requirement.
In the data-model, I've got cafes and owners. Each cafe could be owned by many owners and a cafe could be transferred to some-other owner at a later stage. So the cafes are modelled as /cafes/{cafeId} and everything that belongs to the cafe as /cafes/{cafeId}/items/{itemId} etc.
We also need to query cafes based on different params, if modelled below users it becomes difficult. For these reasons the cafe cannot be modelled as /users/{userId}/cafes/{cafeId}.
As far as security rules are concerned, I could control write access using get(<>) to determine who gets write access to cafes. There is no problem with the security.
I feel that the execution context should provide all available information and let the developers handle it appropriate for their use case. And for serverless apps userId is a must.
If event.auth is not provided in the function, then this restriction will force items that does not belong to users to be modelled /users/{userId}/<item_name>/{itemId} just for the sake of accessing the userId in the cloud functions. This doesn't feel natural.
Also right now there is no way to figure if the cloud function is triggered because of the changes performed in the console. The event.auth info that is available for firebase database triggered functions will be perfect to handle all cases.
Any suggestions regarding how to remodel this case is appreciated as well.
#
Thanks in advance,
I have been facing a similar issue. In Firebase, it was easy - you simply took the data from event.auth. I would assume this is simply a feature not implemented yet while we are in the beta phase of Firestore. Adding the user id to the path does not work as you previously mentioned as it will constantly be changing depending on the user making the update.
My scenario is that I want to create a "lastUpdatedBy" field in the object being updated. If we were to allow the client to send in a lastUpdatedBy field in the payload, this could be abused by a rogue client (i.e. someone with a authenticated account) trying to impersonate someone else. Therefore in Firebase we relied on a cloud function to populate this field on data change events.
My workaround is to allow the client to insert the "lastUpdatedBy" field but additionally use the Firestore rules to validate that the userId in the payload matches that of the logged in user - otherwise deny the write request.
Something like:
match /collectionA/{docId} {
allow update: if request.resource.data.lastUpdatedBy == request.auth.uid;
}
Until Google/Firestore guys add the "auth" object to the cloud function I don't see any other workaround but would love to hear differently.
Since Cloud Functions 1.0 you can get the UID like this
exports.dbCreate = functions.database.ref('/path').onCreate((snap, context) => {
const uid = context.auth.uid;
const authVar = context.auth;
});
Here is a nice post from the FB team for all CF1.0 changes: https://firebase.google.com/docs/functions/beta-v1-diff#event_parameter_split_into_data_and_context
The data of context.auth can be found here: https://firebase.google.com/docs/firestore/reference/security/#properties

Firestore - Cloud Functions - Get uid

I'm trying to get the UID of the user authenticated by firebase web sdk, in the cloud function. The cloud function is triggered by onWrite event of cloud firestore.
This function is triggered when the logged in user is creating/updating items to the cafe. The authentication is handled by Firebase Auth. The security rules enable write only for logged in users. So this event could be tied to a user.
export const cfun = functions.firestore.document('cafes/{cafeId}/items/{itemId}').onWrite(async event => {
// trying to get the uid here
})
There are examples in the docs that deals with the userId, but in all those cases the userId is part of the document path. But in this model the user is not part of the path, as a cafe could have multiple owners and so could be manipulated by many users. So adding userId to the path is not an option.
It looks like a common case for serverless architecture.
#
Update: Functions triggered by firestore doesn't have event.auth populated. Looking for suggestions on modelling the following requirement.
In the data-model, I've got cafes and owners. Each cafe could be owned by many owners and a cafe could be transferred to some-other owner at a later stage. So the cafes are modelled as /cafes/{cafeId} and everything that belongs to the cafe as /cafes/{cafeId}/items/{itemId} etc.
We also need to query cafes based on different params, if modelled below users it becomes difficult. For these reasons the cafe cannot be modelled as /users/{userId}/cafes/{cafeId}.
As far as security rules are concerned, I could control write access using get(<>) to determine who gets write access to cafes. There is no problem with the security.
I feel that the execution context should provide all available information and let the developers handle it appropriate for their use case. And for serverless apps userId is a must.
If event.auth is not provided in the function, then this restriction will force items that does not belong to users to be modelled /users/{userId}/<item_name>/{itemId} just for the sake of accessing the userId in the cloud functions. This doesn't feel natural.
Also right now there is no way to figure if the cloud function is triggered because of the changes performed in the console. The event.auth info that is available for firebase database triggered functions will be perfect to handle all cases.
Any suggestions regarding how to remodel this case is appreciated as well.
#
Thanks in advance,
I have been facing a similar issue. In Firebase, it was easy - you simply took the data from event.auth. I would assume this is simply a feature not implemented yet while we are in the beta phase of Firestore. Adding the user id to the path does not work as you previously mentioned as it will constantly be changing depending on the user making the update.
My scenario is that I want to create a "lastUpdatedBy" field in the object being updated. If we were to allow the client to send in a lastUpdatedBy field in the payload, this could be abused by a rogue client (i.e. someone with a authenticated account) trying to impersonate someone else. Therefore in Firebase we relied on a cloud function to populate this field on data change events.
My workaround is to allow the client to insert the "lastUpdatedBy" field but additionally use the Firestore rules to validate that the userId in the payload matches that of the logged in user - otherwise deny the write request.
Something like:
match /collectionA/{docId} {
allow update: if request.resource.data.lastUpdatedBy == request.auth.uid;
}
Until Google/Firestore guys add the "auth" object to the cloud function I don't see any other workaround but would love to hear differently.
Since Cloud Functions 1.0 you can get the UID like this
exports.dbCreate = functions.database.ref('/path').onCreate((snap, context) => {
const uid = context.auth.uid;
const authVar = context.auth;
});
Here is a nice post from the FB team for all CF1.0 changes: https://firebase.google.com/docs/functions/beta-v1-diff#event_parameter_split_into_data_and_context
The data of context.auth can be found here: https://firebase.google.com/docs/firestore/reference/security/#properties

Resources