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

Follow the instructions in the Add-ons section to install the add-on into your application.

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 Administration → Tenants screen 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:

  1. 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 just alice.

    You can use the jmix.multitenancy.tenantIdUrlParamName application property to specify a different name for the URL parameter.

  2. 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.

  1. 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;
    }
  2. Implement the io.jmix.multitenancy.core.AcceptsTenant interface in the User entity. The getTenantId() method must return the attribute marked with @TenantId annotation:

    public class User implements JmixUserDetails, HasTimeZone, AcceptsTenant {
    
        // ...
        @Override
        public String getTenantId() {
            return tenant;
        }
    }
  3. Add tenant column to the table in user-browse.xml:

    <column id="tenant"/>
  4. Add field for selecting tenant in user-edit.xml:

    <comboBox id="tenantField" property="tenant" editable="false"/>
  5. 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);
        // ...
  6. 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();