I have created a new application in JMIX with multitenancy add-on. I can log in using the admin user but when I try to log in using the user created for a tenant, the log-in fails.
I have followed all the steps recommended as follows:
-
User Entity
@JmixEntity
@Entity(name = “st_User”)
@Table(name = “ST_USER”, indexes = {
@Index(name = “IDX_ST_USER_ON_USERNAME”, columnList = “USERNAME”, unique = true)
})
public class User implements JmixUserDetails, HasTimeZone, AcceptsTenant {
@TenantId
@Column(name = “TENANT”)
private String tenant;@Id @Column(name = "ID", nullable = false) @JmixGeneratedValue private UUID id; .............. .......... @Override public String getTenantId() { return tenant; } public String getTenant() { return tenant; } public void setTenant(String tenant) { this.tenant = tenant; } public UUID getId() { return id; }
-
Added tenant field in both user browser and editor screen
-
User Edit controller
@UiController(“st_User.edit”)
@UiDescriptor(“user-edit.xml”)
@EditedEntityContainer(“userDc”)
@Route(value = “users/edit”, parentPrefix = “users”)
public class UserEdit extends StandardEditor {@Autowired private EntityStates entityStates; @Autowired private PasswordEncoder passwordEncoder; @Autowired private PasswordField passwordField; @Autowired private TextField<String> usernameField; @Autowired private PasswordField confirmPasswordField; @Autowired private Notifications notifications; @Autowired private MessageBundle messageBundle; @Autowired private ComboBox<String> timeZoneField; @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 onInitEntity(InitEntityEvent<User> event) { usernameField.setEditable(true); passwordField.setVisible(true); confirmPasswordField.setVisible(true); tenantField.setEditable(true); } @Subscribe public void onAfterShow(AfterShowEvent event) { if (entityStates.isNew(getEditedEntity())) { usernameField.focus(); } } @Subscribe protected void onBeforeCommit(BeforeCommitChangesEvent event) { if (entityStates.isNew(getEditedEntity())) { if (!Objects.equals(passwordField.getValue(), confirmPasswordField.getValue())) { notifications.create(Notifications.NotificationType.WARNING) .withCaption(messageBundle.getMessage("passwordsDoNotMatch")) .show(); event.preventCommit(); } getEditedEntity().setPassword(passwordEncoder.encode(passwordField.getValue())); } } @Subscribe public void onInit(InitEvent event) { timeZoneField.setOptionsList(Arrays.asList(TimeZone.getAvailableIDs())); tenantField.setOptionsList(multitenancyUiSupport.getTenantOptions()); }
}
-
Updated log-in screen as suggested
@UiController(“st_LoginScreen”)
@UiDescriptor(“login-screen.xml”)
@Route(path = “login”, root = true)
public class LoginScreen extends Screen {@Autowired private TextField<String> usernameField; @Autowired private PasswordField passwordField; @Autowired private CheckBox rememberMeCheckBox; @Autowired private ComboBox<Locale> localesField; @Autowired private Notifications notifications; @Autowired private Messages messages; @Autowired private MessageTools messageTools; @Autowired private LoginScreenSupport loginScreenSupport; @Autowired private UiLoginProperties loginProperties; @Autowired private JmixApp app; private final Logger log = LoggerFactory.getLogger(LoginScreen.class); @Autowired private MultitenancyUiSupport multitenancyUiSupport; @Autowired private UrlRouting urlRouting; @Subscribe private void onInit(InitEvent event) { usernameField.focus(); initLocalesField(); initDefaultCredentials(); } private void initLocalesField() { localesField.setOptionsMap(messageTools.getAvailableLocalesMap()); localesField.setValue(app.getLocale()); localesField.addValueChangeListener(this::onLocalesFieldValueChangeEvent); } private void onLocalesFieldValueChangeEvent(HasValue.ValueChangeEvent<Locale> event) { //noinspection ConstantConditions app.setLocale(event.getValue()); UiControllerUtils.getScreenContext(this).getScreens() .create(this.getClass(), OpenMode.ROOT) .show(); } private void initDefaultCredentials() { String defaultUsername = loginProperties.getDefaultUsername(); if (!StringUtils.isBlank(defaultUsername) && !"<disabled>".equals(defaultUsername)) { usernameField.setValue(defaultUsername); } else { usernameField.setValue(""); } String defaultPassword = loginProperties.getDefaultPassword(); if (!StringUtils.isBlank(defaultPassword) && !"<disabled>".equals(defaultPassword)) { passwordField.setValue(defaultPassword); } else { passwordField.setValue(""); } } @Subscribe("submit") private void onSubmitActionPerformed(Action.ActionPerformedEvent event) { login(); } private void login() { String username = usernameField.getValue(); String password = passwordField.getValue(); if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) { notifications.create(Notifications.NotificationType.WARNING) .withCaption(messages.getMessage(getClass(), "emptyUsernameOrPassword")) .show(); return; } // add tenantId prefix if it was provided in the URL username = multitenancyUiSupport.getUsernameByUrl(username, urlRouting); try { loginScreenSupport.authenticate( AuthDetails.of(username, password) .withLocale(localesField.getValue()) .withRememberMe(rememberMeCheckBox.isChecked()), this); } catch (BadCredentialsException | DisabledException | LockedException e) { log.info("Login failed", e); notifications.create(Notifications.NotificationType.ERROR) .withCaption(messages.getMessage(getClass(), "loginFailed")) .withDescription(messages.getMessage(getClass(), "badCredentials")) .show(); } }
}
-
Only one thing I didn’t find to update is the following recommendation in the user guide:
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();
Thanks for your help.