Skip to content
This repository has been archived by the owner on Mar 31, 2022. It is now read-only.

Commit

Permalink
README review #5
Browse files Browse the repository at this point in the history
  • Loading branch information
andreysubbotin committed Jun 22, 2021
1 parent e7e0dcc commit 7bd7d8f
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 94 deletions.
106 changes: 43 additions & 63 deletions README.md
Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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<String> 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
<column id="tenantId"/>
```

7. Add combobox for tenant in user-edit.xml
7. Add combobox for tenant in `user-edit.xml`

```xml
<comboBox id="tenantIdField" property="tenantAttribute"/>
<comboBox id="tenantIdField" property="tenantId"/>
```

8. Add code in UserEdit class
8. Add code in `UserEdit` class

```java
@Autowired
private ComboBox<String> 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<String> event) {
usernameField.setValue(multitenancyUiSupport.getUsernameByTenant(usernameField.getValue(), event.getValue()));
}
```

9. Add column into table in user-browse.xml

```xml
<column id="tenantAttribute"/>
```

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.
Expand Up @@ -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<String, String> params);
public interface MultitenancyUiSupport {

String getMultitenancyUsername(String username, String newTenantId);
String getUsernameByUrl(String username, UrlRouting urlRouting);

String getUsernameByTenant(String username, String tenantId);

List<String> getTenantOptions();
}
Expand Up @@ -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<String, String> params) {
public String getUsernameByUrl(String username, UrlRouting urlRouting) {
if (Strings.isNullOrEmpty(multitenancyProperties.getTenantIdUrlParamName())) {
return username;
}
Map<String, String> 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<String> 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;
}
}
Expand Up @@ -15,6 +15,6 @@
*/

@Internal
package io.jmix.multitenancyui.helper.impl;
package io.jmix.multitenancyui.impl;

import io.jmix.core.annotation.Internal;
Expand Up @@ -19,13 +19,12 @@
/**
* Needs for implement in user entity for setting tenant id into current authentication
*/
public interface TenantSupport {
public interface AcceptsTenant {

/**
* Provides the user tenant id.
*
* @return the tenant id for user.
*/
String getTenantId();

}
Expand Up @@ -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;

Expand All @@ -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;
}
}
4 changes: 2 additions & 2 deletions multitenancy/src/test/java/test_support/entity/User.java
Expand Up @@ -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;

Expand All @@ -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)
Expand Down

0 comments on commit 7bd7d8f

Please sign in to comment.