Multi-tenant TMS WEB Core client applications with Firestore

TMS Software Delphi  Components

This is the 2nd article in a series of 4 about Firestore features in TMS WEB Core v1.7. Firestore is a cloud based solution from Google allowing to create modern web client backend databases in the Google cloud. So, there is zero config or server setup, all is managed by Google. This article series explores how components in TMS WEB Core facilitate the use of a Google Cloud Firestore backend in your web client applications.

Previous Firestore ClientDataSet component

Before TMS WEB Core v1.7, the built-in dataset component only supported a shared usage of a Firestore collection as a dataset. This means a collection is shared between different users who can sign-in and can access all the records in the collection through the dataset and are able to modify them. 

This kind of shared usage is still supported and may be desirable for certain collections depending on the requirements of the web app. 

But consider the case where you want to develop a web client application where each signed-in user can only see and change his own data. Such a feature is called Multi-tenancy

If you were to develop this kind of Multi-tenant App directly in JavaScript that uses the Firestore API, you will need to devise your own scheme to keep each user’s data separate. A common approach is based on an internal-user-id of the signed-in user that is available from the API. But how your design uses that user-id is entirely up to you.

There are many alternate designs possible to support this kind of Multi-tenancy. One design may use the relational approach of keeping a user-id field with each row or data object (called document in Firestore). Another design may create a nested collection whose name is based on the internal-user-id. 

The new version of TWebFirestoreClientDataSet makes it simple for you by taking care of this internally as described next.

New Multi-tenancy feature in TWebFirestoreClientDataSet 

The new Firestore  specific TWebFirestoreClientDataSet component makes it easy for you to keep the data separate for each signed-in user. You tell it to do so by using the following property:

property UserIdFilter

The new version of the component has an internal implementation of the relational approach described above whereby it can automatically attach the internal-user-id of the signed-in user as an additional field to keep the data separate for the user. 

To get this feature, all you need to do is set the property UserIdFilter as active.

 

fireStoreClientDataSet.UserIdFilter := ufActive;
Once you do that, the component adds a new field named ‘uid’ to each new object (called document in Firestore) to identify the documents belonging to the signed-in user. Similarly when getting the initial list of objects on opening, it filters the collection on the ‘uid’ field. However, this automatic adding of ‘uid’ field happens only when creating new objects or documents. Hence, you may use this feature on a new collection only unless an existing collection already has the data separated by the same scheme–a field containing the internal-user-id. This is described next.

property UserIdFieldName

Suppose you want to connect to a pre-existing collection made by another app with the same scheme where the difference is that the user id field is not named ‘uid’ but is ‘userid’. To connect to such a collection with the feature to view and maintain separate user data, what you need to do is additionally specify a field name to use instead of ‘uid’ by using the property UserIdFieldName.

  fireStoreClientDataSet.UserIdFieldName := 'userid'; 

TMS Software Delphi  Components Security Note: But all this happens at the client. How can we protect the data on Firestore itself?

As already described above, the component implements the above internal-user-id based logic by using a filter to view the data for the current user id and to update the data with a user id field forced in it. But that also means that all the code to do this is on the client side. That poses a security risk because a malicious signed-in user can use the same Firestore API code to get a list of other users’ data by using a filter without the user id condition or by using the internal-user-id of another user in the condition.

How do you prevent that? Firestore does give a solution for this problem at the server end. You can modify the authentication rule in the Firebase console. Here is a sample rule to do that:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow create: if request.auth != null;
      allow read, write: if request.auth != null && request.auth.uid == resource.data.uid;
    }
  }
}

Once you have this rule active, no signed-in user can access another user’s data even by faking the user id. It will generate a permission error.

Important

If you are using another field name by way of property UserIdFieldName, for instance “userid” to store the internal-user-id, please modify the above rule to replace resource.data.uid with resource.data.userid.

How does it work?

The new allow statement for read/write protects any data in which the uid property of a data object does not match the uid of the Signed-In user. This check is not needed for a new record and hence the allow for create operation only checks for a Signed-In user access.

New Multi-Tenant Demo demonstrates the “user specific data” feature

You will find the new Firestore Multi-tenant Demo in the folder “DemoServicesFirestore Multitenant.” To run the Demo and see the above features in action, you need to perform the following steps:

  1. Enable the Email/Password Sign-In method in the authentication section of the Firebase console.
  2. Open the Multi-Tenant Demo and customize the Firebase parameter constants at the top of the Form unit. See details on how to get the parameters by referring to the Firestore documentation in the TMS Web Core developer’s guide. They are basically the same parameters that you may have used to run the Basic TodoList Demo.
  3. Put the following Rule in Firestore console. This is not really needed to run the Demo. But it is required for better security as described earlier.
    rules_version = '2';
    service cloud.firestore {
      match /databases/{database}/documents {
        match /{document=**} {
          allow create: if request.auth != null;
          allow read, write: if request.auth != null && request.auth.uid == resource.data.uid;
        }
      }
    }

  4. Now build and run the Demo. Create a new user account by entering an email and password. Add a few tasks. Then sign out and create another new user, adding another set of tasks for that user. Now sign out and sign in as either of the users. You will see that only the tasks belonging to the signed-in user appear in the data grid.

Note that the Multi-tenant demo creates a new collection by the name “UserTasks” in Firestore. You can change the collection name in the form unit code that sets the CollectionName property.

Here is a screenshot of Multi-tenant demo in action where a user john@doe.com has signed-in and the data grid shows the tasks belonging to this user.

TMS Software Delphi  Components

Multi-Tenant Demo showing tasks for the signed-in user

  • First, we had a brief look at what it means to have Multi-tenancy wherein each signed-in user can only view and modify his own data.
  • Next, we saw the Multi-tenancy feature provided in the new version of the Firestore ClientDataSet component.
  • We also discussed the security aspects and how to implement the security for Multi-tenancy at the Firestore end.
  • Then we looked at the new Multi-Tenant Demo that demonstrates the “user specific data” features.

In the next part of this series, we are going to take a look at the new Filtering features of the Firestore ClientDataSet component that would allow us to limit the data coming in and paginate it as needed.