diff --git a/README.md b/README.md index 69780f8..b671042 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,11 @@ - [Setting](#setting) - [Predefined Roles](#predefined-roles) - [Managing Tenants](#managing-tenants) +- [Authentication](#authentication) - [Common and Tenant-Specific Data](#common-and-tenant-specific-data) - [Common Data](#common-data) - [Tenant-Specific Data](#tenant-specific-data) +- [Usage](#usage) # Overview @@ -25,7 +27,7 @@ All tenants have their own admin users which can create tenant users and assign # Setting -Tenant-specific entity must have an additional attribute which have @TenanId to specify the owner of the data. Note, +Tenant-specific entity must have an additional attribute with `@TenantId` annotation to specify the owner of the data. Note, that the following standard Jmix entities already have an additional column `SYS_TENANT_ID` to support multitenancy: * EntityLogItem @@ -38,7 +40,7 @@ that the following standard Jmix entities already have an additional column `SYS * FilterConfiguration * UiTablePresentation -Entities from CUBA compatibility modules also have special attributes for multitenancy support +Entities from CUBA compatibility modules with multitenancy support: * FileDescriptor * Folder @@ -57,27 +59,19 @@ system entities. To manage tenants go to the *Tenant management -> Tenants* screen. -Tenants are created and managed by global admins - users that don't belong to any tenant. +Global admins create and manage tenants - users that don't belong to any tenant. Each tenant must have a unique *Tenant Id* and a default administrator assigned. -# Tenant Permissions - -Tenant permissions are compiled at runtime during the user logs in and stored in the user session. For implementation, -see `MultiTenancyAttributeConstraint` and `MultiTenancyNonTenantEntityConstraint`. - -If a user has read-only access to an entity, so the user can't permit other users to modify it, but can prohibit users -to read the entity. - -**Specific** and **UI** permissions have been hidden from tenants. - -# Login -You can create users with equal logins for different tenants. +# Authentication +You can create users with equal usernames for different tenants. For each user which belongs to a tenant will be added suffix with tenant id into the username. For example, you will create a user with username - `user1` also it has tenant id - `tenant1`. It means that the user will be saved with username as `tenant1\user1`. -In order to login into an application you can choose one approach for login: -1. You can use the URL parameter when you open the login screen. The parameter name defined by `tenantIdUrlParamName` application property. Now, you can use a username without a tenant suffix. Using the example above you can log in as `user1`. +In order to authenticate into an application you can choose one approach for login: +1. You can use the URL parameter when you open the login screen. To enable this option add parameter `jmix.multitenancy.tenantIdUrlParamName` to the application properties file. + Then add the tenantId parameter to the URL when log in, for example: `http://localhos:8080/#login?tenantId=some_tenant` + Now, you can use a username without a tenant suffix. Using the example above you can log in as `user1`. 2. You can log in with a full username without a URL parameter. For that, you should use a username with a tenant id. For the example above that, we should use `tenant1\user1` for login. # Common and Tenant-Specific Data @@ -88,101 +82,87 @@ Tenants have read-only access to all persistent entities that don't have the att ## Tenant-Specific Data -To be tenant-specific, an entity must have the attribute with @TenantId. +To be tenant-specific, an entity must have the attribute with `@TenantId` annotation. Every time a tenant user reads tenant-specific data, the system adds **where** condition on `TENANT_ID` to JPQL query in order to read the data of the current tenant only. Data with no `TENANT_ID` or with different `TENANT_ID` will be omitted. **There is no automatic filtering for native SQL, so tenants should not have access to any functionality that provides -access to writing native SQL or Groovy code (JMX Console, SQL/Groovy bands in reports etc.)**. +access to writing native SQL or Groovy code (JMX Console, SQL/Groovy bands in the Reports etc.)**. -# Setting empty Jmix project for using multitenancy +# Usage -This is a sample multitenancy project. It creates from a single-module Jmix project if you want to create the same -project you can use the following instructions. - -Steps for getting multitenancy project from empty Jmix project: - -1. Add multitenancy-starter and multitenancy-ui-starter in build.gradle +1. Add `multitenancy-starter` and `multitenancy-ui-starter` in `build.gradle`. ```groovy implementation 'io.jmix.multitenancy:jmix-multitenancy-starter' implementation 'io.jmix.multitenancy:jmix-multitenancy-ui-starter' ``` -2. Add attribute in UserEntity which will be tenant Id, it must have String type and annotation TenantId. For example +2. Add attribute in `User` entity which will be tenant entity, it must have String type and annotation `@TenantId`. ```java @TenantId - @Column(name = "TENANT_ATTRIBUTE") - protected String tenantAttribute; + @Column(name = "TENANT_ID") + protected String tenantId; ``` -3. Implement interface TenantSupport in UserEntity. Method implementation from interface must return value of attribute - marked TenantId annotation +3. Implement interface `io.jmix.multitenancy.core.AcceptsTenant` in `User` entity. Method implementation from interface must return value of attribute + marked with `@TenantId` annotation. ```java @Override public String getTenantId(){ - return tenantAttribute; + return tenantId; } ``` - -4. Add method in UserEdit class - -```java - @Subscribe("tenantIdField") - public void onTenantIdFieldValueChange(HasValue.ValueChangeEvent event) { - usernameField.setValue(multitenancyUsernameSupport.getMultitenancyUsername(usernameField.getValue(), event.getValue())); - } -``` - -5. Add following code in LoginScreen class +4. Add following code in `LoginScreen` class. ```java @Autowired - private MultitenancyUsernameSupport multitenancyUsernameSupport; + private MultitenancyUiSupport multitenancyUiSupport; @Autowired private UrlRouting urlRouting; ``` -6. Add code into 'login' method from LoginScreen class before try-catch block +5. Add code into 'login' method from `LoginScreen` class before try-catch block. ```java - username = multitenancyUsernameSupport.getMultitenancyUsername(username, urlRouting.getState().getParams()); + username = multitenancyUiSupport.getUsernameByUrl(username, urlRouting); +``` +6. Add column into the table in `user-browse.xml` + +```xml + ``` -7. Add combobox for tenant in user-edit.xml +7. Add combobox for tenant in `user-edit.xml` ```xml - + ``` -8. Add code in UserEdit class +8. Add code in `UserEdit` class ```java @Autowired private ComboBox tenantIdField; + @Autowired - private DataManager dataManager; + private MultitenancyUiSupport multitenancyUiSupport; + @Subscribe public void onInit(InitEvent event){ - tenantIdField.setOptionsList(dataManager.load(Tenant.class) - .query("select t from mten_Tenant t") - .list() - .stream() - .map(Tenant::getTenantId) - .collect(Collectors.toList())); + tenantIdField.setOptionsList(multitenancyUiSupport.getTenantOptions()); + } + + @Subscribe("tenantIdField") + public void onTenantIdFieldValueChange(HasValue.ValueChangeEvent event) { + usernameField.setValue(multitenancyUiSupport.getUsernameByTenant(usernameField.getValue(), event.getValue())); } ``` -9. Add column into table in user-browse.xml - -```xml - -``` - -Now you can run your application, create tenant-specific entities, create roles for the entities, assignment roles for +Now you can run your application, create tenant-specific entities, create roles for the entities, assign roles to users and other. \ No newline at end of file diff --git a/multitenancy-ui/src/main/java/io/jmix/multitenancyui/helper/MultitenancyUsernameSupport.java b/multitenancy-ui/src/main/java/io/jmix/multitenancyui/MultitenancyUiSupport.java similarity index 65% rename from multitenancy-ui/src/main/java/io/jmix/multitenancyui/helper/MultitenancyUsernameSupport.java rename to multitenancy-ui/src/main/java/io/jmix/multitenancyui/MultitenancyUiSupport.java index bbbd3ef..b0beb05 100644 --- a/multitenancy-ui/src/main/java/io/jmix/multitenancyui/helper/MultitenancyUsernameSupport.java +++ b/multitenancy-ui/src/main/java/io/jmix/multitenancyui/MultitenancyUiSupport.java @@ -14,14 +14,17 @@ * limitations under the License. */ -package io.jmix.multitenancyui.helper; +package io.jmix.multitenancyui; -import java.util.Map; +import io.jmix.ui.navigation.UrlRouting; -public interface MultitenancyUsernameSupport { +import java.util.List; - String getMultitenancyUsername(String username, Map params); +public interface MultitenancyUiSupport { - String getMultitenancyUsername(String username, String newTenantId); + String getUsernameByUrl(String username, UrlRouting urlRouting); + String getUsernameByTenant(String username, String tenantId); + + List getTenantOptions(); } diff --git a/multitenancy-ui/src/main/java/io/jmix/multitenancyui/helper/impl/MultitenancyUsernameSupportImpl.java b/multitenancy-ui/src/main/java/io/jmix/multitenancyui/impl/MultitenancyUiSupportImpl.java similarity index 53% rename from multitenancy-ui/src/main/java/io/jmix/multitenancyui/helper/impl/MultitenancyUsernameSupportImpl.java rename to multitenancy-ui/src/main/java/io/jmix/multitenancyui/impl/MultitenancyUiSupportImpl.java index 7a67ccf..6577ff7 100644 --- a/multitenancy-ui/src/main/java/io/jmix/multitenancyui/helper/impl/MultitenancyUsernameSupportImpl.java +++ b/multitenancy-ui/src/main/java/io/jmix/multitenancyui/impl/MultitenancyUiSupportImpl.java @@ -14,52 +14,71 @@ * limitations under the License. */ -package io.jmix.multitenancyui.helper.impl; +package io.jmix.multitenancyui.impl; import com.google.common.base.Strings; +import io.jmix.core.DataManager; import io.jmix.multitenancy.MultitenancyProperties; -import io.jmix.multitenancyui.helper.MultitenancyUsernameSupport; +import io.jmix.multitenancy.entity.Tenant; +import io.jmix.multitenancyui.MultitenancyUiSupport; +import io.jmix.ui.navigation.UrlRouting; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; /** * Helper for login screen. */ -@Component("mten_MultitenancyLoginHelper") -public class MultitenancyUsernameSupportImpl implements MultitenancyUsernameSupport { +@Component("mten_MultitenancyUiSupport") +public class MultitenancyUiSupportImpl implements MultitenancyUiSupport { + @Autowired + private MultitenancyProperties multitenancyProperties; + @Autowired + private DataManager dataManager; - private final MultitenancyProperties multitenancyProperties; private static final String TENANT_USERNAME_SEPARATOR = "\\"; - public MultitenancyUsernameSupportImpl(MultitenancyProperties multitenancyProperties) { - this.multitenancyProperties = multitenancyProperties; - } - @Override - public String getMultitenancyUsername(String username, Map params) { + public String getUsernameByUrl(String username, UrlRouting urlRouting) { + if (Strings.isNullOrEmpty(multitenancyProperties.getTenantIdUrlParamName())) { + return username; + } + Map params = urlRouting.getState().getParams(); String tenantId = null; if (params != null) { tenantId = params.get(multitenancyProperties.getTenantIdUrlParamName()); } - return createMultitenancyUsername(username, tenantId); + return concatUsername(username, tenantId); } - private String createMultitenancyUsername(String username, String tenantId) { - if (!Strings.isNullOrEmpty(tenantId)) { - username = String.format("%s%s%s", tenantId, TENANT_USERNAME_SEPARATOR, username); - } - return username; + @Override + public List getTenantOptions() { + return dataManager.load(Tenant.class) + .all() + .list() + .stream() + .map(Tenant::getTenantId) + .collect(Collectors.toList()); } @Override - public String getMultitenancyUsername(String username, String newTenantId) { + public String getUsernameByTenant(String username, String newTenantId) { username = username != null ? username : ""; newTenantId = newTenantId != null ? newTenantId : ""; if (username.contains(TENANT_USERNAME_SEPARATOR)) { username = username.substring(username.indexOf(TENANT_USERNAME_SEPARATOR) + 1); return username; } - return createMultitenancyUsername(username, newTenantId); + return concatUsername(username, newTenantId); + } + + private String concatUsername(String username, String tenantId) { + if (!Strings.isNullOrEmpty(tenantId)) { + username = String.format("%s%s%s", tenantId, TENANT_USERNAME_SEPARATOR, username); + } + return username; } } diff --git a/multitenancy-ui/src/main/java/io/jmix/multitenancyui/helper/impl/package-info.java b/multitenancy-ui/src/main/java/io/jmix/multitenancyui/impl/package-info.java similarity index 87% rename from multitenancy-ui/src/main/java/io/jmix/multitenancyui/helper/impl/package-info.java rename to multitenancy-ui/src/main/java/io/jmix/multitenancyui/impl/package-info.java index d623c7f..cc441fa 100644 --- a/multitenancy-ui/src/main/java/io/jmix/multitenancyui/helper/impl/package-info.java +++ b/multitenancy-ui/src/main/java/io/jmix/multitenancyui/impl/package-info.java @@ -15,6 +15,6 @@ */ @Internal -package io.jmix.multitenancyui.helper.impl; +package io.jmix.multitenancyui.impl; import io.jmix.core.annotation.Internal; \ No newline at end of file diff --git a/multitenancy/src/main/java/io/jmix/multitenancy/core/TenantSupport.java b/multitenancy/src/main/java/io/jmix/multitenancy/core/AcceptsTenant.java similarity index 96% rename from multitenancy/src/main/java/io/jmix/multitenancy/core/TenantSupport.java rename to multitenancy/src/main/java/io/jmix/multitenancy/core/AcceptsTenant.java index 1093364..2b794dd 100644 --- a/multitenancy/src/main/java/io/jmix/multitenancy/core/TenantSupport.java +++ b/multitenancy/src/main/java/io/jmix/multitenancy/core/AcceptsTenant.java @@ -19,7 +19,7 @@ /** * Needs for implement in user entity for setting tenant id into current authentication */ -public interface TenantSupport { +public interface AcceptsTenant { /** * Provides the user tenant id. @@ -27,5 +27,4 @@ public interface TenantSupport { * @return the tenant id for user. */ String getTenantId(); - } diff --git a/multitenancy/src/main/java/io/jmix/multitenancy/core/impl/TenantProviderImpl.java b/multitenancy/src/main/java/io/jmix/multitenancy/core/impl/TenantProviderImpl.java index 33cfb50..9ce7a38 100644 --- a/multitenancy/src/main/java/io/jmix/multitenancy/core/impl/TenantProviderImpl.java +++ b/multitenancy/src/main/java/io/jmix/multitenancy/core/impl/TenantProviderImpl.java @@ -18,7 +18,7 @@ import io.jmix.core.security.CurrentAuthentication; import io.jmix.multitenancy.core.TenantProvider; -import io.jmix.multitenancy.core.TenantSupport; +import io.jmix.multitenancy.core.AcceptsTenant; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; @@ -45,10 +45,10 @@ public String getCurrentUserTenantId() { return TenantProvider.NO_TENANT; } UserDetails userDetails = currentAuthentication.getUser(); - if (!(userDetails instanceof TenantSupport)) { + if (!(userDetails instanceof AcceptsTenant)) { return TenantProvider.NO_TENANT; } - String tenantId = ((TenantSupport) userDetails).getTenantId(); + String tenantId = ((AcceptsTenant) userDetails).getTenantId(); return tenantId != null ? tenantId : TenantProvider.NO_TENANT; } } diff --git a/multitenancy/src/test/java/test_support/entity/User.java b/multitenancy/src/test/java/test_support/entity/User.java index 3af1fd0..fbd3d74 100644 --- a/multitenancy/src/test/java/test_support/entity/User.java +++ b/multitenancy/src/test/java/test_support/entity/User.java @@ -22,7 +22,7 @@ import io.jmix.core.metamodel.annotation.DependsOnProperties; import io.jmix.core.metamodel.annotation.InstanceName; import io.jmix.core.metamodel.annotation.JmixEntity; -import io.jmix.multitenancy.core.TenantSupport; +import io.jmix.multitenancy.core.AcceptsTenant; import io.jmix.security.authentication.JmixUserDetails; import org.springframework.security.core.GrantedAuthority; @@ -37,7 +37,7 @@ @Table(name = "SCR_USER", indexes = { @Index(name = "IDX_SCR_USER_ON_USERNAME", columnList = "USERNAME", unique = true) }) -public class User implements JmixUserDetails, TenantSupport { +public class User implements JmixUserDetails, AcceptsTenant { @Id @Column(name = "ID", nullable = false)