public interface MultiTenancy
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:
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.
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.
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:
$webRoot/shared/ds
RPCManager.setTenantId()
, or setting the servlet attribute
isc_tenantId
$tenantId
in your database config (and of course provisioning the
databases!)DataSources
in Smart GWT support the Bridge model
and Silo model for
multi-tenancy. as described in the section below titled, "Multi-tenancy models."
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.
server.properties
filetenandId
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.
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.
<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.
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 tenantisc_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.
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. |
For consistency, we define the following terms before continuing:
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.
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.