Multitenancy

Дополнение позволяет создавать мультитенантные приложения Jmix, в которых данные нескольких тенантов хранятся в одной базе данных. Один экземпляр приложения может обслуживать несколько тенантов, определяемых как группы пользователей, изолированные друг от друга и имеющие доступ только к определенным (часто только для чтения) данным.

В мультитенантном приложении Jmix существует две основные категории данных:

  • Общие данные:

    • Данные, которые используются совместно всеми тенантами в приложении.

    • Тенанты имеют доступ только для чтения к этим общим данным, которые являются общедоступными, но не могут изменяться отдельными тенантами.

  • Данные конкретного тенанта:

    • Данные, специфичные для каждого тенанта, которые не видны и недоступны другим тенантам.

    • Тенанты имеют полный доступ к своим собственным данным, позволяя им взаимодействовать и изменять эти данные по мере необходимости, не затрагивая данные, принадлежащие другим тенантам.

Реализуя мультитенантность в приложениях Jmix, разработчики могут эффективно управлять сервисами и предоставлять их множеству клиентов или групп пользователей, обеспечивая изоляцию данных, безопасность и персонализированный контроль доступа в соответствии с конкретными потребностями каждого тенанта. Этот подход оптимизирует организацию данных и доступ к ним, сохраняя конфиденциальность и целостность данных в различных группах пользователей.

Дополнение Generic REST не полностью поддерживает мультитенантность. Оно не разделяет экземпляры сущностей по тенантам.

Установка

Для автоматической установки через Jmix Marketplace следуйте инструкциям в разделе Дополнения.

Для ручной установки добавьте следующие зависимости в ваш build.gradle:

implementation 'io.jmix.multitenancy:jmix-multitenancy-starter'
implementation 'io.jmix.multitenancy:jmix-multitenancy-flowui-starter'

Как это работает

В вашем проекте специфичные для тенанта сущности должны включать строковый атрибут, аннотированный @TenantId. Когда пользователь тенанта загружает эти сущности, фреймворк применяет условие WHERE на основе атрибута tenant-id к JPQL-запросу, чтобы выбрать только данные, относящиеся к тенанту пользователя. Кроме того, атрибут tenant-id автоматически присваивается тенанту текущего пользователя при сохранении новых сущностей.

Автоматическая фильтрация для нативного SQL отсутствует, поэтому пользователи тенанта не должны иметь доступ к любым функциям, предоставляющим доступ к нативному SQL или коду Groovy (JMX Console, создание отчетов и т.д.).

В вашем проекте сущность User должна включать атрибут tenant-id. Этому атрибуту должно быть присвоено определенное значение для всех пользователей тенанта. Пользователи без значения в этом атрибуте (те, кто не связан с каким-либо конкретным тенантом) могут получать доступ к данным всех тенантов. Такая настройка подходит для глобальных администраторов, которые отвечают за настройку тенантов и наблюдение за всей системой.

Следующие сущности модулей Jmix имеют атрибут sysTenantId и поддерживают мультитенантность:

  • EntityLogItem

  • SendingMessage

  • SendingAttachment

  • Report

  • ReportGroup

  • ResourceRoleEntity

  • RowLevelRoleEntity

  • FilterConfiguration

Управление тенантами

Дополнение предоставляет экран Multitenancy → Tenants, который позволяет глобальным администраторам создавать и изменять тенанты.

Сущность регистрации тенанта имеет два атрибута:

  • Tenant id - идентификатор, используемый в специфичных для тенанта сущностях. Не может быть изменен после создания.

  • Tenant name - описательное имя тенанта.

tenants view

Пользователи тенанта

В мультитенантных приложениях пользователи в разных тенантах могут иметь одинаковые логины. Чтобы гарантировать уникальность атрибута username во всем приложении, пользователям тенанта следует регистрироваться с логином, включающим префикс tenant-id. Например, если есть два разных пользователя с именем Alice в тенантах t1 и t2, их логины должны быть t1|alice и t2|alice соответственно.

Пользователи тенанта могут входить в приложение, указывая полный логин, включающий tenant-id, например, t1|alice.

Вы можете реализовать собственную схему уникальных логинов вместо описанного выше метода.

Настройка пользователей

В этом разделе мы объясним процесс настройки управления пользователями и аутентификации в вашем проекте для поддержки мультитенантности.

  1. Добавьте строковый атрибут в вашу сущность User и аннотируйте его аннотацией @TenantId:

    @TenantId
    @Column(name = "TENANT")
    private String tenant;
    
    public String getTenant() {
        return tenant;
    }
    
    public void setTenant(String tenant) {
        this.tenant = tenant;
    }
  2. Добавьте колонку tenant в таблицу данных в user-list-view.xml:

    <column property="tenant"/>
  3. Добавьте поле для выбора тенанта в user-detail-view.xml:

    <comboBox id="tenantField" property="tenant" readOnly="true"/>
  4. Добавьте следующее в класс UserDetailView:

    @ViewComponent
    private JmixComboBox<String> tenantField;
    
    @Autowired
    private TenantProvider tenantProvider;
    
    @Autowired
    private MultitenancyUiSupport multitenancyUiSupport;
    
    @Subscribe
    public void onInit(final InitEvent event) {
        // ...
        tenantField.setItems(multitenancyUiSupport.getTenantOptions());
    }
    
    @Subscribe
    public void onInitEntity(final InitEntityEvent<User> event) {
        // ...
        tenantField.setReadOnly(false);
    }
    
    @Subscribe
    public void onBeforeShow(final BeforeShowEvent event) {
        // ...
        String currentTenantId = tenantProvider.getCurrentUserTenantId();
        if (!currentTenantId.equals(TenantProvider.NO_TENANT)
                && Strings.isNullOrEmpty(tenantField.getValue())) {
            tenantField.setReadOnly(true);
            tenantField.setValue(currentTenantId);
        }
    }
    
    @Subscribe("tenantField")
    public void onTenantFieldComponentValueChange(final AbstractField.ComponentValueChangeEvent<JmixComboBox<String>, String> event) {
        usernameField.setValue(
                multitenancyUiSupport.getUsernameByTenant(usernameField.getValue(), event.getValue())
        );
    }
  5. Чтобы разрешить использование идентичных логинов в различных тенантах, как было объяснено ранее, включите следующий фрагмент кода в ваш класс LoginView:

    @Autowired
    private MultitenancyUiSupport multitenancyUiSupport;
    
    private Location currentLocation; (1)
    
    @Override
    public void beforeEnter(BeforeEnterEvent event) {
        currentLocation = event.getLocation();
        super.beforeEnter(event);
    }
    
    @Subscribe("login")
    public void onLogin(final LoginEvent event) {
        String username = multitenancyUiSupport.getUsernameByLocation(event.getUsername(), currentLocation);
    
        try {
            loginViewSupport.authenticate(
                    AuthDetails.of(username, event.getPassword())
                            .withLocale(login.getSelectedLocale())
                            .withRememberMe(login.isRememberMe())
            );
        } catch (BadCredentialsException | DisabledException | LockedException | AccessDeniedException e) {
            log.info("Login failed", e);
            event.getSource().setError(true);
        }
    }
    1 Используйте объект com.vaadin.flow.router.Location для работы с текущим URL.

Настройка безопасности

При настройке ролей для пользователей тенанта не включайте атрибуты tenant-id в политики атрибутов сущностей, чтобы скрыть их от пользователей. Например, если сущность Customer специфична для определенного тенанта и включает атрибут tenant, аннотированный @TenantId, роль, предоставляющая доступ к этой сущности, должна явно указывать атрибуты и исключать атрибут tenant:

@ResourceRole(name = "UsersRole", code = UsersRole.CODE, scope = "UI")
public interface UsersRole {
    String CODE = "users-role";

    @EntityAttributePolicy(entityClass = Customer.class,
            attributes = {"id", "name", "region", "version"},
            action = EntityAttributePolicyAction.MODIFY)
    @EntityPolicy(entityClass = Customer.class, actions = EntityPolicyAction.ALL)
    void customer();
}