Browse Source

Add users filter to enforce 2FA for

pull/14043/head
ViacheslavKlimov 1 year ago
parent
commit
16102d22aa
  1. 4
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  2. 6
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java
  3. 44
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  4. 6
      application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java
  5. 15
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  6. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java
  7. 4
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
  8. 9
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  9. 14
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  10. 2
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  11. 12
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  12. 50
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  13. 1
      common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java
  14. 6
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  15. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java
  16. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java
  17. 19
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemLevelUsersFilter.java
  18. 2
      common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java
  19. 3
      common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java
  20. 2
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java
  21. 51
      dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java
  22. 1
      dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java
  23. 80
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

4
application/src/main/java/org/thingsboard/server/controller/AuthController.java

@ -226,8 +226,8 @@ public class AuthController extends BaseController {
}
JwtPair tokenPair;
if (twoFactorAuthService.isEnforceTwoFaEnabled(securityUser.getTenantId())) {
tokenPair = authenticationSuccessHandler.createMfaTokenPair(securityUser, Authority.ENFORCE_MFA_TOKEN);
if (twoFactorAuthService.isEnforceTwoFaEnabled(securityUser.getTenantId(), user)) {
tokenPair = authenticationSuccessHandler.createMfaTokenPair(securityUser, Authority.MFA_CONFIGURATION_TOKEN);
} else {
tokenPair = tokenFactory.createTokenPair(securityUser);
}

6
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthConfigController.java

@ -97,7 +97,7 @@ public class TwoFactorAuthConfigController extends BaseController {
"Will throw an error (Bad Request) if the provider is not configured for usage. " +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PostMapping("/account/config/generate")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'ENFORCE_MFA_TOKEN')")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')")
public TwoFaAccountConfig generateTwoFaAccountConfig(@Parameter(description = "2FA provider type to generate new account config for", schema = @Schema(defaultValue = "TOTP", requiredMode = Schema.RequiredMode.REQUIRED))
@RequestParam TwoFaProviderType providerType) throws Exception {
SecurityUser user = getCurrentUser();
@ -137,7 +137,7 @@ public class TwoFactorAuthConfigController extends BaseController {
"Will throw an error (Bad Request) if the provider is not configured for usage. " +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PostMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'ENFORCE_MFA_TOKEN')")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')")
public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig,
@RequestParam(required = false) String verificationCode) throws Exception {
SecurityUser user = getCurrentUser();
@ -194,7 +194,7 @@ public class TwoFactorAuthConfigController extends BaseController {
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER
)
@GetMapping("/providers")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'ENFORCE_MFA_TOKEN')")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER', 'MFA_CONFIGURATION_TOKEN')")
public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException {
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true)
.map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream()

44
application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java

@ -28,7 +28,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
@ -90,7 +89,14 @@ public class TwoFactorAuthController extends BaseController {
@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
return getRegularJwtPair(servletRequest, user, verificationSuccess, "Verification code is incorrect");
if (verificationSuccess) {
logLogInAction(servletRequest, user, null);
return createTokenPair(user);
} else {
IllegalArgumentException error = new IllegalArgumentException("Verification code is incorrect");
logLogInAction(servletRequest, user, error);
throw error;
}
}
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
@ -129,28 +135,32 @@ public class TwoFactorAuthController extends BaseController {
.collect(Collectors.toList());
}
@ApiOperation(value = "Get regular token pair after successfully saved two factor settings",
notes = "Checks 2FA setting saved, and if it success the method returns a regular access and refresh token pair.")
@ApiOperation(value = "Get regular token pair after successfully configuring 2FA",
notes = "Checks 2FA is configured, returning token pair on success.")
@PostMapping("/login")
@PreAuthorize("hasAuthority('ENFORCE_MFA_TOKEN')")
public JwtPair authorizeByTwoFaEnforceToken(HttpServletRequest servletRequest) throws ThingsboardException {
@PreAuthorize("hasAuthority('MFA_CONFIGURATION_TOKEN')")
public JwtPair authenticateByTwoFaConfigurationToken(HttpServletRequest servletRequest) throws ThingsboardException {
SecurityUser user = getCurrentUser();
boolean isEnabled = twoFactorAuthService.isTwoFaEnabled(user.getTenantId(), user.getId());
return getRegularJwtPair(servletRequest, user, isEnabled, "Two factor settings is not set up!");
}
private JwtPair getRegularJwtPair(HttpServletRequest servletRequest, SecurityUser user, boolean isAvailable, String errorMessage) throws ThingsboardException {
if (isAvailable) {
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, null);
user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal());
return tokenFactory.createTokenPair(user);
if (twoFactorAuthService.isTwoFaEnabled(user.getTenantId(), user.getId())) {
logLogInAction(servletRequest, user, null);
return createTokenPair(user);
} else {
ThingsboardException error = new ThingsboardException(errorMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error);
IllegalArgumentException error = new IllegalArgumentException("2FA is not configured");
logLogInAction(servletRequest, user, error);
throw error;
}
}
private JwtPair createTokenPair(SecurityUser user) {
log.debug("[{}][{}] Creating token pair for user", user.getTenantId(), user.getId());
user = new SecurityUser(userService.findUserById(user.getTenantId(), user.getId()), true, user.getUserPrincipal());
return tokenFactory.createTokenPair(user);
}
private void logLogInAction(HttpServletRequest servletRequest, SecurityUser user, Exception error) {
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error);
}
@Data
@AllArgsConstructor
@Builder

6
application/src/main/java/org/thingsboard/server/service/security/auth/ForceMfaAuthenticationToken.java → application/src/main/java/org/thingsboard/server/service/security/auth/MfaConfigurationToken.java

@ -1,5 +1,5 @@
/**
* Copyright © 2016-2024 The Thingsboard Authors
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -17,8 +17,8 @@ package org.thingsboard.server.service.security.auth;
import org.thingsboard.server.service.security.model.SecurityUser;
public class ForceMfaAuthenticationToken extends AbstractJwtAuthenticationToken {
public ForceMfaAuthenticationToken(SecurityUser securityUser) {
public class MfaConfigurationToken extends AbstractJwtAuthenticationToken {
public MfaConfigurationToken(SecurityUser securityUser) {
super(securityUser);
}
}

15
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.limit.LimitedApi;
import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
@ -68,10 +69,16 @@ public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
}
@Override
public boolean isEnforceTwoFaEnabled(TenantId tenantId) {
return configManager.getPlatformTwoFaSettings(tenantId, true)
.map(PlatformTwoFaSettings::isEnforceTwoFa)
.orElse(false);
public boolean isEnforceTwoFaEnabled(TenantId tenantId, User user) {
SystemLevelUsersFilter enforcedUsersFilter = configManager.getPlatformTwoFaSettings(TenantId.SYS_TENANT_ID, true)
.filter(PlatformTwoFaSettings::isEnforceTwoFa)
.map(PlatformTwoFaSettings::getEnforcedUsersFilter)
.orElse(null);
if (enforcedUsersFilter == null) {
return false;
}
return userService.matchesFilter(tenantId, enforcedUsersFilter, user);
}
@Override

2
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java

@ -27,7 +27,7 @@ public interface TwoFactorAuthService {
boolean isTwoFaEnabled(TenantId tenantId, UserId userId);
boolean isEnforceTwoFaEnabled(TenantId tenantId);
boolean isEnforceTwoFaEnabled(TenantId tenantId, User user);
void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException;

4
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java

@ -167,8 +167,8 @@ public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
for (TwoFaProviderConfig providerConfig : twoFactorAuthSettings.getProviders()) {
twoFactorAuthService.checkProvider(tenantId, providerConfig.getProviderType());
}
if (twoFactorAuthSettings.isEnforceTwoFa() && twoFactorAuthSettings.getProviders().isEmpty()) {
throw new DataValidationException("At least one 2FA provider is required if enforce enabled!");
if (tenantId.isSysTenantId() && twoFactorAuthSettings.isEnforceTwoFa() && twoFactorAuthSettings.getProviders().isEmpty()) {
throw new DataValidationException("At least one 2FA provider is required if enforcing is enabled");
}
AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.orElseGet(() -> {

9
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java

@ -43,7 +43,7 @@ import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.ForceMfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.MfaConfigurationToken;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.exception.UserPasswordNotValidException;
@ -83,11 +83,10 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
Assert.notNull(authentication, "No authentication data provided");
Object principal = authentication.getPrincipal();
if (!(principal instanceof UserPrincipal)) {
if (!(principal instanceof UserPrincipal userPrincipal)) {
throw new BadCredentialsException("Authentication Failed. Bad user principal.");
}
UserPrincipal userPrincipal = (UserPrincipal) principal;
SecurityUser securityUser;
if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
String username = userPrincipal.getValue();
@ -106,8 +105,8 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
securityUser = authenticateByUsernameAndPassword(authentication, userPrincipal, username, password);
if (twoFactorAuthService.isTwoFaEnabled(securityUser.getTenantId(), securityUser.getId())) {
return new MfaAuthenticationToken(securityUser);
} else if (twoFactorAuthService.isEnforceTwoFaEnabled(securityUser.getTenantId())) {
return new ForceMfaAuthenticationToken(securityUser);
} else if (twoFactorAuthService.isEnforceTwoFaEnabled(securityUser.getTenantId(), securityUser)) {
return new MfaConfigurationToken(securityUser);
} else {
systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null);
}

14
application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java

@ -15,11 +15,11 @@
*/
package org.thingsboard.server.service.security.auth.rest;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
@ -29,8 +29,8 @@ import org.springframework.stereotype.Component;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.service.security.auth.ForceMfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.MfaConfigurationToken;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -39,7 +39,7 @@ import java.io.IOException;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Component(value = "defaultAuthenticationSuccessHandler")
@Slf4j @Component(value = "defaultAuthenticationSuccessHandler")
@RequiredArgsConstructor
public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final JwtTokenFactory tokenFactory;
@ -47,15 +47,14 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
Authentication authentication) throws IOException {
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
JwtPair tokenPair;
if (authentication instanceof MfaAuthenticationToken) {
tokenPair = createMfaTokenPair(securityUser, Authority.PRE_VERIFICATION_TOKEN);
}
else if (authentication instanceof ForceMfaAuthenticationToken) {
tokenPair = createMfaTokenPair(securityUser, Authority.ENFORCE_MFA_TOKEN);
} else if (authentication instanceof MfaConfigurationToken) {
tokenPair = createMfaTokenPair(securityUser, Authority.MFA_CONFIGURATION_TOKEN);
} else {
tokenPair = tokenFactory.createTokenPair(securityUser);
}
@ -68,6 +67,7 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc
}
public JwtPair createMfaTokenPair(SecurityUser securityUser, Authority scope) {
log.debug("[{}][{}] Creating {} token", securityUser.getTenantId(), securityUser.getId(), scope);
JwtPair tokenPair = new JwtPair();
int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true)
.flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())

2
application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java

@ -136,7 +136,7 @@ public class JwtTokenFactory {
}
UserPrincipal principal;
if (authority != Authority.PRE_VERIFICATION_TOKEN && authority != Authority.ENFORCE_MFA_TOKEN) {
if (authority != Authority.PRE_VERIFICATION_TOKEN && authority != Authority.MFA_CONFIGURATION_TOKEN) {
securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
securityUser.setLastName(claims.get(LAST_NAME, String.class));
securityUser.setEnabled(claims.get(ENABLED, Boolean.class));

12
application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java

@ -138,6 +138,7 @@ import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.data.security.DeviceCredentialsType;
import org.thingsboard.server.common.data.security.model.JwtPair;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
import org.thingsboard.server.common.data.tenant.profile.TenantProfileData;
import org.thingsboard.server.common.msg.session.FeatureType;
@ -203,7 +204,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected static final String TENANT_ADMIN_PASSWORD = "tenant";
protected static final String DIFFERENT_TENANT_ADMIN_EMAIL = "testdifftenant@thingsboard.org";
private static final String DIFFERENT_TENANT_ADMIN_PASSWORD = "difftenant";
protected static final String DIFFERENT_TENANT_ADMIN_PASSWORD = "difftenant";
protected static final String CUSTOMER_USER_EMAIL = "testcustomer@thingsboard.org";
private static final String CUSTOMER_USER_PASSWORD = "customer";
@ -596,8 +597,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
Assert.assertNotNull(tokenInfo);
Assert.assertTrue(tokenInfo.has("token"));
Assert.assertTrue(tokenInfo.has("refreshToken"));
String token = tokenInfo.get("token").asText();
String refreshToken = tokenInfo.get("refreshToken").asText();
validateAndSetJwtToken(JacksonUtil.treeToValue(tokenInfo, JwtPair.class), username);
}
protected void validateAndSetJwtToken(JwtPair jwtPair, String username) {
Assert.assertNotNull(jwtPair);
String token = jwtPair.getToken();
String refreshToken = jwtPair.getRefreshToken();
validateJwtToken(token, username);
validateJwtToken(refreshToken, username);
this.token = token;

50
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java

@ -34,6 +34,7 @@ import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.audit.AuditLog;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
@ -59,6 +60,7 @@ import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -67,10 +69,6 @@ import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
@ -121,7 +119,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
public void testTwoFa_totp() throws Exception {
TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa();
logInWithPreVerificationToken(username, password);
logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
doPost("/api/auth/2fa/verification/send?providerType=TOTP")
.andExpect(status().isOk());
@ -141,7 +139,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
public void testTwoFa_sms() throws Exception {
configureSmsTwoFa();
logInWithPreVerificationToken(username, password);
logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
doPost("/api/auth/2fa/verification/send?providerType=SMS")
.andExpect(status().isOk());
@ -165,7 +163,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
twoFaSettings.setTotalAllowedTimeForVerification(65);
});
logInWithPreVerificationToken(username, password);
logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
await("expiration of the pre-verification token")
.atLeast(Duration.ofSeconds(30).plusMillis(500))
@ -182,7 +180,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
});
logInWithPreVerificationToken(username, password);
logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
Stream.generate(() -> StringUtils.randomNumeric(6))
.limit(9)
@ -211,7 +209,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
twoFaSettings.setMinVerificationCodeSendPeriod(10);
});
logInWithPreVerificationToken(username, password);
logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
doPost("/api/auth/2fa/verification/send?providerType=TOTP")
.andExpect(status().isOk());
@ -235,7 +233,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
twoFaSettings.setVerificationCodeCheckRateLimit("3:10");
});
logInWithPreVerificationToken(username, password);
logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
for (int i = 0; i < 3; i++) {
String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect")
@ -263,7 +261,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
@Test
public void testCheckVerificationCode_invalidVerificationCode() throws Exception {
configureTotpTwoFa();
logInWithPreVerificationToken(username, password);
logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) {
String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + invalidVerificationCode)
@ -278,7 +276,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
smsTwoFaProviderConfig.setVerificationCodeLifetime(10);
});
logInWithPreVerificationToken(username, password);
logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
doPost("/api/auth/2fa/verification/send?providerType=SMS").andExpect(status().isOk());
@ -301,7 +299,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
public void testTwoFa_logLoginAction() throws Exception {
TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa();
logInWithPreVerificationToken(username, password);
logInWithMfaToken(username, password, Authority.PRE_VERIFICATION_TOKEN);
await("async audit log saving").during(1, TimeUnit.SECONDS);
doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect")
@ -383,7 +381,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
emailTwoFaAccountConfig.setEmail(twoFaUser.getEmail());
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), emailTwoFaAccountConfig);
logInWithPreVerificationToken(twoFaUser.getEmail(), "12345678");
logInWithMfaToken(twoFaUser.getEmail(), "12345678", Authority.PRE_VERIFICATION_TOKEN);
Map<TwoFaProviderType, TwoFactorAuthController.TwoFaProviderInfo> providersInfos = readResponse(doGet("/api/auth/2fa/providers").andExpect(status().isOk()), new TypeReference<List<TwoFactorAuthController.TwoFaProviderInfo>>() {}).stream()
.collect(Collectors.toMap(TwoFactorAuthController.TwoFaProviderInfo::getType, v -> v));
@ -401,7 +399,7 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
}
@Test
public void testEnforceTwoFactorSetting() throws Exception {
public void testEnforceTwoFa() throws Exception {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
@ -410,14 +408,13 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
twoFaSettings.setMinVerificationCodeSendPeriod(5);
twoFaSettings.setTotalAllowedTimeForVerification(100);
twoFaSettings.setEnforceTwoFa(true);
TenantAdministratorsFilter enforcedUsersFilter = new TenantAdministratorsFilter();
enforcedUsersFilter.setTenantsIds(Set.of(tenantId.getId()));
twoFaSettings.setEnforcedUsersFilter(enforcedUsersFilter);
twoFaSettings = twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
JsonNode node = readResponse(doPost("/api/auth/login", new LoginRequest(username, password)).andExpect(status().isOk()), JsonNode.class);
assertNotNull(node.get("token").asText());
assertNull(node.get("refreshToken"));
assertEquals(node.get("scope").asText(), Authority.ENFORCE_MFA_TOKEN.name());
logInWithMfaToken(username, password, Authority.MFA_CONFIGURATION_TOKEN);
this.token = node.get("token").asText();
TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, totpTwoFaProviderConfig.getProviderType());
String secret = UriComponentsBuilder.fromUriString(totpTwoFaAccountConfig.getAuthUrl()).build()
.getQueryParams().getFirst("secret");
@ -425,24 +422,27 @@ public class TwoFactorAuthTest extends AbstractControllerTest {
readResponse(doPost("/api/2fa/account/config?verificationCode=" + verificationCode, totpTwoFaAccountConfig).andExpect(status().isOk()), JsonNode.class);
JwtPair tokenPair = readResponse(doPost("/api/auth/2fa/login").andExpect(status().isOk()), JwtPair.class);
assertNotNull(tokenPair);
assertThat(tokenPair.getToken()).isNotEmpty();
assertThat(tokenPair.getRefreshToken()).isNotEmpty();
validateAndSetJwtToken(tokenPair, username);
this.token = tokenPair.getToken();
this.refreshToken = tokenPair.getRefreshToken();
doGet("/api/user/" + user.getId()).andExpect(status().isOk());
// verifying enforced users filter
createDifferentTenant();
doGet("/api/user/" + user.getId()).andExpect(status().isOk());
twoFaSettings.setEnforceTwoFa(false);
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
}
private void logInWithPreVerificationToken(String username, String password) throws Exception {
private void logInWithMfaToken(String username, String password, Authority expectedScope) throws Exception {
LoginRequest loginRequest = new LoginRequest(username, password);
JwtPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtPair.class);
assertThat(response.getToken()).isNotNull();
assertThat(response.getRefreshToken()).isNull();
assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN);
assertThat(response.getScope()).isEqualTo(expectedScope);
this.token = response.getToken();
}

1
common/dao-api/src/main/java/org/thingsboard/server/dao/tenant/TbTenantProfileCache.java

@ -15,7 +15,6 @@
*/
package org.thingsboard.server.dao.tenant;
import org.thingsboard.server.common.data.SystemParams;
import org.thingsboard.server.common.data.TenantProfile;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;

6
common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java

@ -23,6 +23,8 @@ import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UserCredentialsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.UserCredentials;
@ -109,4 +111,8 @@ public interface UserService extends EntityDaoService {
void removeMobileSession(TenantId tenantId, String mobileToken);
PageData<User> findUsersByFilter(TenantId tenantId, UsersFilter filter, PageLink pageLink);
boolean matchesFilter(TenantId tenantId, SystemLevelUsersFilter filter, User user);
}

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/AllUsersFilter.java

@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.notification.targets.platform;
import lombok.Data;
@Data
public class AllUsersFilter implements UsersFilter {
public class AllUsersFilter implements SystemLevelUsersFilter {
@Override
public UsersFilterType getType() {

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemAdministratorsFilter.java

@ -18,7 +18,7 @@ package org.thingsboard.server.common.data.notification.targets.platform;
import lombok.Data;
@Data
public class SystemAdministratorsFilter implements UsersFilter {
public class SystemAdministratorsFilter implements SystemLevelUsersFilter {
@Override
public UsersFilterType getType() {

19
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/SystemLevelUsersFilter.java

@ -0,0 +1,19 @@
/**
* Copyright © 2016-2025 The Thingsboard Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.common.data.notification.targets.platform;
public interface SystemLevelUsersFilter extends UsersFilter {
}

2
common/data/src/main/java/org/thingsboard/server/common/data/notification/targets/platform/TenantAdministratorsFilter.java

@ -21,7 +21,7 @@ import java.util.Set;
import java.util.UUID;
@Data
public class TenantAdministratorsFilter implements UsersFilter {
public class TenantAdministratorsFilter implements SystemLevelUsersFilter {
private Set<UUID> tenantsIds;
private Set<UUID> tenantProfilesIds;

3
common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java

@ -20,9 +20,10 @@ public enum Authority {
SYS_ADMIN(0),
TENANT_ADMIN(1),
CUSTOMER_USER(2),
REFRESH_TOKEN(10),
PRE_VERIFICATION_TOKEN(11),
ENFORCE_MFA_TOKEN(12);
MFA_CONFIGURATION_TOKEN(12);
private int code;

2
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java

@ -21,6 +21,7 @@ import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
@ -47,6 +48,7 @@ public class PlatformTwoFaSettings {
private Integer totalAllowedTimeForVerification;
private boolean enforceTwoFa;
private SystemLevelUsersFilter enforcedUsersFilter;
public Optional<TwoFaProviderConfig> getProviderConfig(TwoFaProviderType providerType) {
return Optional.ofNullable(providers)

51
dao/src/main/java/org/thingsboard/server/dao/notification/DefaultNotificationTargetService.java

@ -25,17 +25,13 @@ import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.NotificationTargetId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.notification.NotificationRequestStatus;
import org.thingsboard.server.common.data.notification.NotificationType;
import org.thingsboard.server.common.data.notification.info.RuleOriginatedNotificationInfo;
import org.thingsboard.server.common.data.notification.targets.NotificationTarget;
import org.thingsboard.server.common.data.notification.targets.NotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.PlatformUsersNotificationTargetConfig;
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilterType;
import org.thingsboard.server.common.data.page.PageData;
@ -50,9 +46,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
@Service
@Slf4j
@ -115,49 +108,7 @@ public class DefaultNotificationTargetService extends AbstractEntityService impl
@Override
public PageData<User> findRecipientsForNotificationTargetConfig(TenantId tenantId, PlatformUsersNotificationTargetConfig targetConfig, PageLink pageLink) {
UsersFilter usersFilter = targetConfig.getUsersFilter();
switch (usersFilter.getType()) {
case USER_LIST: {
List<User> users = ((UserListFilter) usersFilter).getUsersIds().stream()
.limit(pageLink.getPageSize())
.map(UserId::new).map(userId -> userService.findUserById(tenantId, userId))
.filter(Objects::nonNull).collect(Collectors.toList());
return new PageData<>(users, 1, users.size(), false);
}
case CUSTOMER_USERS: {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
throw new IllegalArgumentException("Customer users target is not supported for system administrator");
}
CustomerUsersFilter filter = (CustomerUsersFilter) usersFilter;
return userService.findCustomerUsers(tenantId, new CustomerId(filter.getCustomerId()), pageLink);
}
case TENANT_ADMINISTRATORS: {
TenantAdministratorsFilter filter = (TenantAdministratorsFilter) usersFilter;
if (!tenantId.equals(TenantId.SYS_TENANT_ID)) {
return userService.findTenantAdmins(tenantId, pageLink);
} else {
if (isNotEmpty(filter.getTenantsIds())) {
return userService.findTenantAdminsByTenantsIds(filter.getTenantsIds().stream()
.map(TenantId::fromUUID).collect(Collectors.toList()), pageLink);
} else if (isNotEmpty(filter.getTenantProfilesIds())) {
return userService.findTenantAdminsByTenantProfilesIds(filter.getTenantProfilesIds().stream()
.map(TenantProfileId::new).collect(Collectors.toList()), pageLink);
} else {
return userService.findAllTenantAdmins(pageLink);
}
}
}
case SYSTEM_ADMINISTRATORS:
return userService.findSysAdmins(pageLink);
case ALL_USERS: {
if (!tenantId.equals(TenantId.SYS_TENANT_ID)) {
return userService.findUsersByTenantId(tenantId, pageLink);
} else {
return userService.findAllUsers(pageLink);
}
}
default:
throw new IllegalArgumentException("Recipient type not supported");
}
return userService.findUsersByFilter(tenantId, usersFilter, pageLink);
}
@Override

1
dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java

@ -78,6 +78,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService<TenantId, Ten
@Autowired
private AdminSettingsService adminSettingsService;
@Autowired
@Lazy
private NotificationSettingsService notificationSettingsService;
@Autowired
private QrCodeSettingService qrCodeSettingService;

80
dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

@ -44,6 +44,11 @@ import org.thingsboard.server.common.data.id.UserCredentialsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.mobile.MobileSessionInfo;
import org.thingsboard.server.common.data.mobile.UserMobileSessionInfo;
import org.thingsboard.server.common.data.notification.targets.platform.CustomerUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.SystemLevelUsersFilter;
import org.thingsboard.server.common.data.notification.targets.platform.TenantAdministratorsFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UserListFilter;
import org.thingsboard.server.common.data.notification.targets.platform.UsersFilter;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.security.Authority;
@ -62,6 +67,7 @@ import org.thingsboard.server.dao.service.DataValidator;
import org.thingsboard.server.dao.service.PaginatedRemover;
import org.thingsboard.server.dao.settings.SecuritySettingsService;
import org.thingsboard.server.dao.sql.JpaExecutorService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import java.util.ArrayList;
import java.util.Collections;
@ -71,7 +77,9 @@ import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static org.apache.commons.collections4.CollectionUtils.isNotEmpty;
import static org.thingsboard.server.common.data.StringUtils.generateSafeToken;
import static org.thingsboard.server.dao.service.Validator.validateId;
import static org.thingsboard.server.dao.service.Validator.validatePageLink;
@ -97,6 +105,7 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
private final UserSettingsService userSettingsService;
private final UserSettingsDao userSettingsDao;
private final SecuritySettingsService securitySettingsService;
private final TbTenantProfileCache tenantProfileCache;
private final DataValidator<User> userValidator;
private final DataValidator<UserCredentials> userCredentialsValidator;
private final ApplicationEventPublisher eventPublisher;
@ -496,6 +505,77 @@ public class UserServiceImpl extends AbstractCachedEntityService<UserCacheKey, U
return userCredentialsDao.incrementFailedLoginAttempts(tenantId, userId);
}
@Override
public PageData<User> findUsersByFilter(TenantId tenantId, UsersFilter filter, PageLink pageLink) {
switch (filter.getType()) {
case USER_LIST -> {
List<User> users = ((UserListFilter) filter).getUsersIds().stream()
.limit(pageLink.getPageSize())
.map(UserId::new).map(userId -> findUserById(tenantId, userId))
.filter(Objects::nonNull).collect(Collectors.toList());
return new PageData<>(users, 1, users.size(), false);
}
case CUSTOMER_USERS -> {
if (tenantId.equals(TenantId.SYS_TENANT_ID)) {
throw new IllegalArgumentException("Customer users target is not supported for system administrator");
}
CustomerUsersFilter customerUsersFilter = (CustomerUsersFilter) filter;
return findCustomerUsers(tenantId, new CustomerId(customerUsersFilter.getCustomerId()), pageLink);
}
case TENANT_ADMINISTRATORS -> {
TenantAdministratorsFilter tenantAdministratorsFilter = (TenantAdministratorsFilter) filter;
if (!tenantId.equals(TenantId.SYS_TENANT_ID)) {
return findTenantAdmins(tenantId, pageLink);
} else {
if (isNotEmpty(tenantAdministratorsFilter.getTenantsIds())) {
return findTenantAdminsByTenantsIds(tenantAdministratorsFilter.getTenantsIds().stream()
.map(TenantId::fromUUID).collect(Collectors.toList()), pageLink);
} else if (isNotEmpty(tenantAdministratorsFilter.getTenantProfilesIds())) {
return findTenantAdminsByTenantProfilesIds(tenantAdministratorsFilter.getTenantProfilesIds().stream()
.map(TenantProfileId::new).collect(Collectors.toList()), pageLink);
} else {
return findAllTenantAdmins(pageLink);
}
}
}
case SYSTEM_ADMINISTRATORS -> {
return findSysAdmins(pageLink);
}
case ALL_USERS -> {
if (!tenantId.equals(TenantId.SYS_TENANT_ID)) {
return findUsersByTenantId(tenantId, pageLink);
} else {
return findAllUsers(pageLink);
}
}
default -> throw new IllegalArgumentException("Recipient type not supported");
}
}
@Override
public boolean matchesFilter(TenantId tenantId, SystemLevelUsersFilter filter, User user) {
switch (filter.getType()) {
case TENANT_ADMINISTRATORS -> {
TenantAdministratorsFilter tenantAdministratorsFilter = (TenantAdministratorsFilter) filter;
if (isNotEmpty(tenantAdministratorsFilter.getTenantsIds())) {
return tenantAdministratorsFilter.getTenantsIds().contains(user.getTenantId().getId());
} else if (isNotEmpty(tenantAdministratorsFilter.getTenantProfilesIds())) {
return tenantAdministratorsFilter.getTenantProfilesIds().contains(tenantProfileCache.get(user.getTenantId()).getUuidId());
} else {
return user.getAuthority() == Authority.TENANT_ADMIN;
}
}
case SYSTEM_ADMINISTRATORS -> {
return user.getAuthority() == Authority.SYS_ADMIN;
}
case ALL_USERS -> {
return true;
}
default -> throw new IllegalArgumentException("Recipient type not supported");
}
}
private void updatePasswordHistory(UserCredentials userCredentials) {
JsonNode additionalInfo = userCredentials.getAdditionalInfo();
if (!(additionalInfo instanceof ObjectNode)) {

Loading…
Cancel
Save