How to check if an authenticated user belong to a specific tenant in a multi-tenant app

1,620 views
Skip to first unread message

Viggo Navarsete

unread,
Feb 16, 2019, 11:02:47 AM2/16/19
to Firebase Google Group
Hi,

I'm working on a multi-tenant saas solution, and have the following proposal for the data structure:

tenants/<autogenerated ID>
organizationNumber: 111111111
organizationName: Company A
Address: Some address

users/<autogenerated ID> 
tenantId: <link to an autogenerated ID from tenants>
Name: User A

orders/<autogenerated ID>
tenantId: <link to an autogenerated ID from tenants>
orderDetails: some order details, can be more fields here


Question 1: Would it be better to use the value of the organizationNumber instead of the autogenerated ID under tenants?

Question 2: When a user logs in using Firestore Authentication, the first thing that happens afterwords is that the client (written in React) tries to retrieve the organization details of the user (meaning the info for the tenant he/she belongs to). I would like to have a rule that checks if the user that authenticated actually belongs to the tenant he/she tries to read, and if not, not allow access.
 My initial proposal for such a security rule looks like this, but I'm not sure if it's secure enough..
 
service cloud.firestore {
  match
/databases/{database}/documents {    
    match
/tenants/{tenantId} {
     allow read
: if exists(/databases/$(database)/documents/users/$(request.auth.uid)) &&
     
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.tenantId == tenantId
   
}
 
}
}

 
 
 
 
 

Alexander Dunlop

unread,
Feb 18, 2019, 12:03:24 AM2/18/19
to Firebase Google Group
Hey Viggo,

Question 1:
You can use the organizationNumber as the ID, Firebase allows for you to set your own document IDs.
The good thing about a <autogenerated ID> is that you can be sure that it will not override any documents.
As you said you are working on a multi-tenant saas solution, so it's a good idea to stick to autogenerated ID.

The work around for getting documents by organizationNumber is something like this
// Create a reference to the tenants collection
var tenantsRef = db.collection("tenants");

// Create a query against the collection.
var query = tenantsRef.where("organizationNumber", "==", "<example-organization-id>").limit(1);
Something to note is that collection query's pricing is based on the documents returned not based on the documents searched.

Question 2: As for the rules
Something to note is that whenever you get a document from the database that counts as a query.
So a would suggest that instead of having the tenantId on the user document.
Have the userId on the tenant document instead.
This would lower the amount of queries to the database.

service cloud.firestore {
  match
/databases/{database}/documents {

   
match /tenants/{tenantId} {
      // Allow read access on to tenants document if tenant.userId is user's id
      allow read
: if resource.data.userId ==
request.auth.uid;
    }
  }
}


Cheers,
Alex

Viggo Navarsete

unread,
Feb 18, 2019, 9:27:00 AM2/18/19
to Firebase Google Group
Thanks for great input Alex!

Where would you put orders that belong to each tenant? As I mentioned in the first post
/orders/<autogenerated ID>
   tenantId

or under /tenants/<autogenerated ID>

pros/cons?

Regards
Viggo

Luca Faggianelli

unread,
Feb 18, 2019, 9:27:00 AM2/18/19
to Firebase Google Group
Viggo,

I'm using a similar schema for my multi-tenant app and it works... Actually I'm using the approach suggested by Alexander for the question 2, I have a list of users with their role in the specific organization (what you call tenant) document, you can use an array as well if you don't need fine grained permissions:

/organizations/123abc/
  users
: {
    j324oior
: 'some.role'
 
}

Alexander Dunlop

unread,
Feb 18, 2019, 4:43:53 PM2/18/19
to Firebase Google Group
Hey Viggo,
If you do not need to do a query against everyones orders I would think about adding a sub-collection.
/tenants/<autogenerated ID>/orders/<autogenerated ID>
Take a look at https://firebase.google.com/docs/firestore/data-model on how better to model your data.

The pros of putting orders in a sub-collection is it's easy to query, you can just get all orders for a tenant.
It's easy for you to manage also, say you want to delete a tenant it will delete all of their orders.

The cons of putting orders in a sub-collection, is you can not query by all orders very easily if you need to.
If you wanted to change the tenant for an order you would need create & delete.
If you wanted to delete a tenant it would delete all of their orders (the work around being archive don't delete) 

If you do not like the idea of a sub-collection you can keep your orders having the tenantId
/orders/<autogenerated ID>
     tenantId
This won't effect how you had it set up before at all, you will still get orders by tenantId

Cheers,
Alex

Viggo Navarsete

unread,
Feb 19, 2019, 9:42:26 AM2/19/19
to Firebase Google Group
Thanks!

I never need to query orders across tenants, so it seems a sub-collection is the way to go. Assume/hope setting up security rules also is easier this way 😊

Viggo Navarsete

unread,
Feb 23, 2019, 11:20:43 AM2/23/19
to Firebase Google Group
Alexander and Luca,

is this a way of doing it, based on your input?

Screenshot from 2019-02-23 13-48-12.png


an array of users within the tenant, with a role and a userId (which points to /user/<autogenerated ID>
a subcollection of orders within the tenant

Luca Faggianelli

unread,
Feb 23, 2019, 2:13:38 PM2/23/19
to Firebase Google Group
Almost, personally I store users within tenants inside an object instead of array, the object keys are the user ids so you easily access this value. The tenant document has a field like this:

users: {
 ezri
....: 'ADMIN', // Role as a string
 sT0z
...: { role: 'EDITOR', joinedOn: '30 Jan 2019' } // or as object

}

Updataing the role is very easy:

firestore.doc('/tenants/123').update({
 
'users.ezri1231': 'EDITOR'
})

A note about subcollections, I decided to completely skip them because you have to delete each subcollection on your own before deleting the parent document, otherwise the subcollection still exists and it's accessible by its path, though you don't see it in firestore console (because the parent is not there), basically you will have a lot of orphan subcollections that eat space in your DB. Anyway your schema would work with orders as subc.

Alexander Dunlop

unread,
Feb 26, 2019, 12:09:06 AM2/26/19
to Firebase Google Group
Hey Viggo,

It is very common to have users in their own table. What is the reasoning behind having a users array like that?
If you add users as a field to the tenants document, and that tenant document ends up having 100 users.
You are now getting all 100 users when you may just need the tenant address name.

"The maximum size is roughly 1 Megabyte, storing such a large number of objects (maps) inside of a single document is generally a bad design (and 20 million is beyond the size limit anyway).

You should reconsider why they need to be in a document, rather than each object being their own document.
Cloud Firestore's limits are listed in the documentation." - Dan McGrath on SO 

What if a user is apart of multiple tenants in the future? What if you want to sign in via the users array, you would have to search every single tenant document (not a good idea).
So what's the reasoning for not having a users table?
Instead you could have a userIds array in the tenant document and then when you get the tenant document you are not getting every user object. Or you could add a tenantId/tenantIds to the user document.
Remember querying a collection pricing is not based on the number of documents needing to be searched it's based of the documents returned. 

Cheers,
Alex

Alexander Dunlop

unread,
Feb 26, 2019, 12:09:06 AM2/26/19
to Firebase Google Group
Hey Luca,

I responded to Viggo just now on why storing users within the tenants inside and object or an array isn't a good idea.
Say you want to do an admin operation of banning a user without knowing the tenant you would have to search every tenant document which is not a good idea. Also this seems like a work around for not using the firestore rich queries.

Your note about subcollections, "I decided to completely skip them because you have to delete each subcollection on your own before deleting the parent document". Are you sure this is correct i have never experienced this before and if you have experienced you should report this as a bug. When I delete my documents with subcollections I am unable to access them at all. Orphan subcollections sounds like something is going wrong.

Cheers,
Alex

Luca Faggianelli

unread,
Feb 26, 2019, 9:52:38 AM2/26/19
to fireba...@googlegroups.com
Ah yes Alexander, sure! sorry I missed a part in my description: I have a users collection where each doc is the user info, then on the tenant document, in the users field I store the members of that tenant, just the user ID. I also store the tenant IDs under each user doc in the users collection to have all user tenants in the user doc. The users collection is the central place where to manage the user (profile, preferences, etc.).

About subcollection deletion, this is well known by firebase team, it's highlighted in the documentation:

They also published a workaround for deleting subcollections

--
You received this message because you are subscribed to a topic in the Google Groups "Firebase Google Group" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/firebase-talk/0POxgDoQgVA/unsubscribe.
To unsubscribe from this group and all its topics, send an email to firebase-tal...@googlegroups.com.
To post to this group, send email to fireba...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/firebase-talk/09521395-6faa-4bcf-a065-b34029920d63%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Viggo Navarsete

unread,
Feb 26, 2019, 9:52:40 AM2/26/19
to Firebase Google Group
Hi Alexander, 

and thanks for detailed feedback:) I do have the users in their own collection:

users_firebase.png






And then in the tenant I have an array holding only the userId and role. Is this also not preferrable? The reason, as you can see in the start of the thread, is that I want to be able to check using security rules if the authenticated user belongs to the tenant he/she wants to read.

tenants_firebase.png


The orders however, I've selected a sub-collection within the tenant.

orders_within_tenant_firebase.png

David DeRemer

unread,
Feb 27, 2019, 3:51:50 PM2/27/19
to Firebase Google Group
Lots in this thread, so I apologize if this was already suggested or not useful to you, but it seems relevant...

You might also consider adding the tenantId to the custom claims on the user's auth object: https://firebase.google.com/docs/auth/admin/custom-claims

That way your security rule could be something like:

allow read: if user.auth.token.tenantId === tenantId
Reply all
Reply to author
Forward
Message has been deleted
0 new messages