Multitenancy
Данный раздел может содержать устаревшую информацию о пользовательском интерфейсе. Она будет скоро обновлена. |
The add-on allows you to build multitenant Jmix applications that store data of all tenants in a single database. A single instance of the application serves multiple tenants - groups of users that are invisible to each other and share only specific (usually read-only) data.
There are two types of data in a multitenant application:
-
Common data shared across tenants. Tenant users should have read-only access to this type of data.
-
Tenant-specific data not accessible to other tenants. Tenant users have full access to this type of data.
Installation
For automatic installation through Jmix Marketplace, follow instructions in the Add-ons section.
For manual installation, add the following dependencies to your build.gradle
:
implementation 'io.jmix.multitenancy:jmix-multitenancy-starter'
implementation 'io.jmix.multitenancy:jmix-multitenancy-flowui-starter'
How it Works
Tenant-specific entities of your project must have an attribute of type String annotated with @TenantId
. When a tenant user loads such entities, the framework adds WHERE
condition by the tenant-id attribute to the JPQL query in order to read only data of the user’s tenant. Also, the tenant-id attribute is automatically set to the current users’s tenant when saving new entities.
There is no automatic filtering for native SQL, so tenant users should not have access to any functionality that provides access to native SQL or Groovy code (JMX Console, creating reports, etc.). |
The User
entity of your project must have a tenant-id attribute. This attribute must be set to a particular value for all tenant users. Users without a value in this attribute (that is not belonging to any tenant) can see data of all tenants, which makes sense for global administrators who can set up tenants and maintain the whole system.
The following entities of Jmix modules have sysTenantId
attribute and support multitenancy:
-
EntityLogItem
-
SendingMessage
-
SendingAttachment
-
Report
-
ReportGroup
-
ResourceRoleEntity
-
RowLevelRoleEntity
-
FilterConfiguration
-
UiTablePresentation
Managing Tenants
The add-on provides Multitenancy → Tenants view which allows global administrators to create and edit tenants.
The tenant registration entity has two attributes:
-
Tenant id - identifier that will be used in tenant-specific entities. It cannot be changed after creation.
-
Tenant name - a descriptive name of the tenant.
Tenant Users
Tenants can have users with the same login names. To provide uniqueness of the username
attribute throughout the whole application, tenant users should be registered with the tenant-id prefix in username. For example, if there are two different Alice users in t1
and t2
tenants, they should have t1|alice
and t2|alice
username respectively.
There are two ways to log in tenant users to the application:
-
Use an URL parameter specifying tenant-id when opening the login screen, for example
http://localhost:8080/#login?tenantId=t1
. Then users can enter their login name without the tenant-id prefix, for example justalice
.You can use the
jmix.multitenancy.tenantIdUrlParamName
application property to specify a different name for the URL parameter. -
Users can provide the full username including the tenant-id, for example
t1|alice
.
You can implement your own scheme of unique usernames instead of the described above. |
Configuring Users
In this section, we describe how to configure the user management and authentication in your project to support multitenancy.
-
Add a string attribute to your
User
entity and annotate it with@TenantId
:@TenantId @Column(name = "TENANT") private String tenant; public String getTenant() { return tenant; } public void setTenant(String tenant) { this.tenant = tenant; }
-
Implement the
io.jmix.multitenancy.core.AcceptsTenant
interface in theUser
entity. ThegetTenantId()
method must return the attribute marked with@TenantId
annotation:public class User implements JmixUserDetails, HasTimeZone, AcceptsTenant { // ... @Override public String getTenantId() { return tenant; } }
-
Add
tenant
column to the table inuser-browse.xml
:<column id="tenant"/>
-
Add field for selecting tenant in
user-edit.xml
:<comboBox id="tenantField" property="tenant" editable="false"/>
-
Add the following to the
UserEdit
class:@Autowired private ComboBox<String> tenantField; @Autowired private TenantProvider tenantProvider; @Autowired private MultitenancyUiSupport multitenancyUiSupport; @Subscribe public void onBeforeShow(BeforeShowEvent event) { String currentTenantId = tenantProvider.getCurrentUserTenantId(); if (!currentTenantId.equals(TenantProvider.NO_TENANT) && Strings.isNullOrEmpty(tenantField.getValue())) { tenantField.setEditable(false); tenantField.setValue(currentTenantId); } } @Subscribe("tenantField") public void onTenantFieldValueChange(HasValue.ValueChangeEvent<String> event) { usernameField.setValue( multitenancyUiSupport.getUsernameByTenant( usernameField.getValue(), event.getValue())); } @Subscribe public void onInit(InitEvent event) { tenantField.setOptionsList(multitenancyUiSupport.getTenantOptions()); // ... @Subscribe public void onInitEntity(InitEntityEvent<User> event) { tenantField.setEditable(true); // ...
-
To support identical login names in different tenants as described above, add the following to your
LoginScreen
class:@Autowired private MultitenancyUiSupport multitenancyUiSupport; @Autowired private UrlRouting urlRouting; private void login() { String username = usernameField.getValue(); // ... // add tenantId prefix if it was provided in the URL username = multitenancyUiSupport.getUsernameByUrl(username, urlRouting); try { loginScreenSupport.authenticate(
Configuring Security
When configuring roles for tenant users, exclude tenant-id attributes from entity attribute policies, so users won’t see them. For example, if the Customer
entity is tenant-specific and has tenant
attribute annotated with @TenantId
, the role that gives access to the entity should list the attributes explicitly and omit tenant
:
@ResourceRole(name = "Users", code = "users", scope = "UI")
public interface UsersRole {
// ...
@EntityAttributePolicy(
entityClass = Customer.class, attributes = {"region", "name", "version", "id"},
action = EntityAttributePolicyAction.MODIFY)
@EntityPolicy(entityClass = Customer.class, actions = EntityPolicyAction.ALL)
void customer();