Interface MultiTenancy


public interface MultiTenancy

Transparent Multi-Tenancy

A multi-tenant application is one where multiple customers experience the same application but with their data segregated - the different customers are not aware of each other.

Smart GWT provides a "transparent" multi-tenant system, in which a Smart GWT application can be quickly converted to support multi-tenancy, with only a handful of very simple changes required.

In short, you designate a set of DataSources as multi-tenant, and those DataSources are then available to you in tenant-specific forms, which can store data in different databases, automatically.

Specifically:

  1. database configurations become a template where tenantId can appear in the settings

    You can use $tenantId in your DB settings so that you can put different tenants in different schemas or even entirely different DB hosts.

    There are also APIs for providing entirely different database settings on a per-tenant basis, or for specific tenants that are special.

  2. when loading DataSources into the browser, you pass a tenantId (to DataSourceLoader, or in JSP tags)

    This means that tenant-specific DataSources are returned, with a naming convention that defaults to mt_tenantId_originalDataSourceID.

    However, for client-side logic, the DataSources are still available under the original name. So, your ListGrid that is bound to the "orders" DataSource still works as expected, and code such as DataSource.get("orders").fetchData(...) also works, unchanged.

  3. for server-side logic, you just tell us the authenticated tenantId, and server-side APIs transparently work without code changes

    You provide the authenticated tenantId to the server-side RPCManager object via the setTenantId() API. From then on, if you ask the RPCManager for a DataSource (getDataSource() API), you get tenant-specific DataSources.

    Likewise, if you create a new DSRequest(...) and pass the RPCManager into the constructor, the operation is scoped to the current tenant.


Taken together, these features mean that a typical Smart GWT app can be converted to multi-tenancy by just:

  1. moving multi-tenant DataSources to the designated directory, by default $webRoot/shared/ds
  2. adding the tenantId to the DataSourceLoader <script> tag in your bootstrap .html file
  3. securely providing the authenticated tenantId to Smart GWT: a call to RPCManager.setTenantId(), or setting the servlet attribute isc_tenantId
  4. adding $tenantId in your database config (and of course provisioning the databases!)
All the rest is handled for you, by Smart GWT.

Smart GWT multi-tenancy

DataSources in Smart GWT support the Bridge model and Silo model for multi-tenancy. as described in the section below titled, "Multi-tenancy models."

Multi-tenant DataSource loading

By default multi-tenant DataSources in Smart GWT are named mt_<tenantId>_<baseId>, where baseId is the filename prefix of the XML DS definition file (e.g. supplyItem for supplyItem.ds.xml), but when loaded by the client an alias is automatically created so that it's accessible simply as baseId via DataSource.get(). The server looks for the definitions under the tenants subdirectory of each of your project.datasources paths, so if that property is just the directory $webRoot/shared/ds, for example, then we'll look in $webRoot/shared/ds/tenants.

When loading a DataSource via the DataSourceLoader servlet, you should supply the baseId(s) of any multi-tenant DataSources. If a tenantId parameter is included in the request, and is authorized, the multi-tenant directories are searched first, falling back to the normal DataSource filesystem location(s) if the DS isn't found. Conversely, if no tenantId parameter is included, the server will search the normal DS location(s) first, and then fall back to the multi-tenant directories (assuming authorization is present).

You can designate specific multi-tenant DataSources as "test" DataSources that should be loaded from the normal DS filesystem location(s) by declaring them in the config property tenant.testDataSources, and you can limit the allowed tenants by listing them in the config property mt.tenants.

Note: See the server.properties table at the end of this topic for how to customize the attribute name and other multi-tenant configuration.

Converting app to use multi-tenancy

To convert an existing Smart GWT app to use multi-tenancy you just need to make a few changes outlined as follows. There's no need to rewrite the client app since an alias is created for the multi-tenant DataSource under its "base" ID.

Define a database template in your server.properties file

When encountering a new tenant for the first time, the Smart GWT server will automatically add a database definition for the tenant to the global config, using the database template. The template consists of a number of separate config properties that are each Velocity templates, so they can reference tenandId and standard Velocity variables, but also existing server.properties variables.

For example, the template used by the multi-tenant sample in the SDK is:

  tenant.dbTemplate.driver: $sql.HSQLDB.driver
  tenant.dbTemplate.driver.databaseName: mt_\$tenantId
  tenant.dbTemplate.driver.url: jdbc:hsqldb:file:$sql.HSQLDB.database.baseDir/mt_\$tenantId
  tenant.dbTemplate.database.type: $sql.HSQLDB.database.type
  tenant.dbTemplate.interface.type: $sql.HSQLDB.interface.type
  tenant.dbTemplate.driver.user: SA
  tenant.dbTemplate.driver.password:
Note that references to Velocity variables must be escaped by putting a backslash before the $ symbol; otherwise it will be interpreted as a reference to another config property.

Add an authorization mechanism

In a multi-tenant system, one tenant's data should not be accessible by other tenants, so you will need to add a means of authenticating the current tenant. There are several ways to do this securely in a production system. We support two approaches: In the first approach, your filter will need to intercept servlet traffic to the IDACall and DataSourceLoader servlets, adding authorization by setting the isc_tenantId attribute on the servlet request. For proper security, your solution should avoid just checking for certain query params, at least if their values can be easily guessed, since that would allow any client to gain access.

Under this approach, when loading DataSources with a <isomorphic:loadDS> tag, there's no need to set the tenantId attribute as it's assumed to be whatever your servlet filter authorizes, though you can set the attribute to "none" to avoid unintentionally picking up a multi-tenant DS if you have one by the same name that's both multi-tenant and a normal DataSource.

As an alternate approach, you can subclass the IDACall servlet, and override IDACall.prepareRPCTransaction() like so:

  public void prepareRPCTransaction (RPCManager rpc, RequestContext context) {
      String tenantId = null;
      // ... code here that analyzes context and sets (authorizes) tenantid ...
      rpc.setTenantId(tenantId);
  }
which also requires setting actionURL to "[ISOMORPHIC]/AuthedIDACall", assuming your subclass is named AuthedIDACall.

Under this approach, you can load the DataSource either by providing a servlet filter to auth DataSourceLoader, or by launching your app with a JSP so that the JSP sets the authorized tenant into the tenantId attribute of the <isomorphic:loadDS> tag.

Multi-tenant demo app

The Order Management App has been included in the Smart GWT SDK as a demo of multi-tenancy. A few shortcuts have been taken adding multi-tenancy to this app, leveraging the fact that we are not worried about a client gaining unauthorized access to the demo tenant data:
  • To enable tenant authorization for the <isomorphic:loadDS> JSP tag, we've just added the built-in ParamMapperFilter filter to the SDK's web.xml to apply the isc_tenantId servlet request parameter to the isc_tenantId servlet request attribute.
  • To support multi-tenant IDACall requests, we've added the config property tenant.testOnlyURLParam to server.properties. This causes any isc_tenantId query param in the page URL to be sent to the server as a request parameter for IDACall requests, and ultimately set into the isc_tenantId servlet request attribute, authorizing the tenant
The end result is that for the demo, you can choose (authorize) one of the two supported tenants, "biginc" and "worldship," by simply setting the tenant's ID as the value of the isc_tenantId query param in the demo URL.

So, for example, you can hit

/examples/multi_tenant/orderManagementJS.jsp?isc_tenantId=biginc

to use the app as tenant "biginc", and

/examples/multi_tenant/orderManagementJS.jsp?isc_tenantId=worldship

to use it as "worldship." Obviously, having the tenant ID query param on the client authorize access is only suitable for a demo, and not a real production setup.

Custom configuration

Much of Smart GWT's multi-tenant behavior can be customized via server.properties configuration:
Property Name Default value Description
datasources.poolTenantDataSources true Whether to pool multi-tenant DataSources
mt.tenants empty list If defined, limits tenants to those listed. Other tenants will be treated as unauthorized even if the normal auth process has been followed.
tenant.datasources list of directories formed from project.datasources by adding the subdirectory /tenants to each directory entry List of directories in which to look for the multi-tenant DS XML definition files (files of the form <baseId>.ds.xml).
tenant.datasources.nameTemplate mt_${tenantId}_$baseId The multi-tenant name for the DataSource with the base DS ID baseId and tenant tenantId. This property is used by the DataSourceLoader servlet to generate a multi-tenant DataSource ID
tenant.datasources.namingPattern mt_([A-Za-z0-9]+)_([A-Za-z0-9_$]+) Pattern used by the Multi-tenant Dynamic DataSource Generator to recognize the name of a valid multi-tenant DataSource. The first regex group will be assumed to be the tenant ID and the second to be the base DS ID.
tenant.dbNameTemplate mt_$tenantId Database name for tenant tenantId
tenant.servletReqAttribute isc_tenantId Attribute to look for on the servlet request that holds the authorized tenant ID. Note that currently, the URL query param name read by the client framework and sent to the server as the request param when tenant.testOnlyURLParam is true is hardcoded to isc_tenantId, and is thus unaffected by this config setting.
tenant.testDataSources empty list List of DataSource base IDs that should be picked up from the normal DS filesystem location(s) (typically defined by project.datasources) instead of from the multi-tenant directories defined by tenant.datasources.
tenant.testOnlyURLParam false If true, when the client URL query parameter isc_tenantId is present and forwarded to the server as a servlet request parameter, it will be set into the servlet request attribute defined by tenant.servletReqAttribute, thus authorizing the tenant specified on the client. This is insecure, and so for testing or demo purposes only.

Multi-tenancy models

Multi-tenancy refers to the ability of a single database system to serve multiple tenants, where each tenant represents a distinct entity, such as a customer, user, or organization. These tenants share the same underlying database infrastructure, but their data is logically isolated and segregated to ensure privacy, security, and performance.

For consistency, we define the following terms before continuing:

  • Database Process: means a separate OS-level database process - what results when you run the mariaDB binary, for example
  • Database Server: means a distinct OS install that is dedicated to one database process, with no other database processes running on that OS. Does not necessarily mean separate physical hardware. Does not necessarily mean that the database server is running only a database process and nothing else (there could also be an application server, for example)
  • Database (without other qualifiers): a container for tables, views, etc, where there might be many "databases" within a given database process . This is what you get when you run "create database" in MariaDB, for example. Sometimes also called a "schema" but we won't use that term, because in Smart GWT we have Component Schema et al and a set of DataSource fields is also commonly referred to as a "schema".
Given these terms, we can describe three different approaches to multi-tenancy.

Pool Model

In the pool model, tenants share and their data coexists within a single database and database process. This is cost efficient, as resources are shared among multiple tenants, and easily scalable to provision for a larger or smaller number of tenants. it's also easily managed, as only a single database process is required.

However, sharing resources with other tenants can raise security risks if not properly managed, and there can be performance issues from resource contention among multiple tenants.

Silo Model

In the silo model, each tenant's data is stored within a dedicated database process, completely isolated from other tenants. In this model, isolating resources for each tenant reduces the risk of security vulnerabilities. Moreover, tenants have more control over their individual resource allocations and configurations. Resource contention between the separate database processes of different tenants may be eliminated, as there's no requirement that the separate processes even be on the same physical hardware.

However, these advantages come with higher costs due to maintaining per-tenant resources. Also, managing the separate database processes or servers can require more effort, and more expertise if different database server products are used. If the separate database processes must run on a single machine, scalability may be an issue as each process may require significant resources.

Bridge Model

The bridge model is a compromise in which data for separate tenants is stored in separate databases within the same database process. This offers better cross-tenant data security than the pool model, while not consuming as many machine resources as having separate database processes per tenant. It can be more easily scaled to a larger number of tenants than a silo implementation, but the separate tenant databases may make management more challenging than a pool implementation.