2
votes

Our current Firestore structure is as follows: enter image description here

  • 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.

3

3 Answers

6
votes

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.

0
votes

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.

0
votes

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.