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:
- 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.
- 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. - 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:
- moving multi-tenant DataSources to the designated directory, by default
$webRoot/shared/ds
- adding the tenantId to the DataSourceLoader <script> tag in your bootstrap .html file
- securely providing the authenticated tenantId to Smart GWT: a call to
RPCManager.setTenantId()
, or setting the servlet attributeisc_tenantId
- adding
$tenantId
in your database config (and of course provisioning the databases!)
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 namedmt_<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:- By Servlet Request. Set an attribute on the servlet request via a web.xml filter or other server code.
- By RPCManager. Call RPCManager.setTenantId() from a user override of IDACall.prepareRPCTransaction()
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 theisc_tenantId
servlet request parameter to theisc_tenantId
servlet request attribute. - To support multi-tenant IDACall requests, we've added the config property
tenant.testOnlyURLParam
toserver.properties
. This causes anyisc_tenantId
query param in the page URL to be sent to the server as a request parameter for IDACall requests, and ultimately set into theisc_tenantId
servlet request attribute, authorizing the tenant
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".
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.