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'

Важное примечание для приложений в режиме меню Single mode

Если ваше приложение использует режим меню Single mode, вам следует вручную добавить пункты меню после установки дополнения.

  1. Откройте Дизайнер меню в Jmix Studio.

  2. Перейдите на вкладку Structure.

  3. Доступные пункты меню отображаются на левой панели.

    add menu items
  4. Чтобы добавить пункты:

    • Выберите нужные пункты и нажмите кнопку > кнопка (Add Selected).

    • Или перетащите их с левой панели в текущую структуру меню справа.

Этот шаг необходим, потому что приложения в режиме Single mode не выполняют автоматическое слияние меню дополнений с существующей структурой.

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

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

Вы можете добавить этот атрибут, используя черту Has Tenant ID в дизайнере сущностей Studio.

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

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

Дополнения и модули с поддержкой Multitenancy

Если используется дополнение Multitenancy, следующие дополнения и модули хранят информацию о тенанте и разделяют свои данные по тенантам, за исключением случаев, отмеченных ниже. Для каждого дополнения и модуля перечислены сущности с атрибутом tenant-id.

Application Settings

Сущности, которые расширяют AppSettingsEntity. Пока тенант не сохранит собственные настройки, его пользователи получают глобальные значения.

Audit

EntityLogItem, EntitySnapshot. Экран Пользовательские сессии отображает сессии всех тенантов.

BPM

ContentStorage, UserGroup, UserGroupRole, UserGroupUser.

Email Sending

SendingMessage, SendingAttachment.

Message Templates

MessageTemplate, MessageTemplateBlock, MessageTemplateGroup.

Notifications

InAppNotification. Уведомлению присваивается тенант его получателя.

Reports

Report, ReportGroup. История выполнения отчетов не разделяется по тенантам, поэтому пользователи тенанта с доступом к ней могут видеть записи других тенантов и их выходные документы.

Подсистема безопасности (встроена во фреймворк)

ResourceRoleEntity, RowLevelRoleEntity. Коды ролей должны быть уникальными среди всех тенантов.

Конфигурация фильтров (встроена во фреймворк)

FilterConfiguration.

Чтобы оценить поддержку Multitenancy в кастомных дополнениях:

  • Проверьте документацию дополнения на наличие упоминаний sysTenantId или @TenantId.

  • Изучите сущности в дополнении, чтобы узнать, есть ли у них атрибут тенанта и как он используется.

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

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

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

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

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

tenants view

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

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

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

Существует два способа входа пользователей аренды в приложение:

  1. Первый способ - использование параметра URL, который включает tenant-id при доступе к экрану входа, например http://localhost:8080/login?tenantId=t1. В этом случае пользователи могут просто ввести своё имя для входа без префикса tenant-id, например, просто alice.

    У вас также есть возможность задать пользовательское имя для параметра URL с помощью свойства приложения jmix.multitenancy.tenantIdUrlParamName application.

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

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

Замещение пользователей

Пользователь, принадлежащий определённому тенанту, должен замещаться только пользователями из того же тенанта.

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

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

  1. Добавьте строковый атрибут в вашу сущность User и аннотируйте его с помощью @TenantId. Вы также можете добавить этот атрибут, используя черту Has Tenant ID в Studio:

    @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();
}

Назначение ролей

Назначение ролей ограничено пользователями в рамках одного тенанта.

При назначении ролей в интерфейсе администратора система обеспечивает изоляцию тенантов следующим образом:

  1. В экране Application → Users

    При использовании кнопки Role assignment в экране Application → Users:

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

    • Роли из других тенантов автоматически скрываются и не отображаются.

  2. В экранах Security → Resource Roles / Security → Row-level Roles

    При использовании кнопки Assign to users в экранах Security → Resource roles или Security → Row-level roles:

    • В диалоге выбора отображаются пользователи из всех тенантов.

    • Однако система не позволяет подтвердить выбор, если выбраны пользователи из разных тенантов.

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