Browse Source

Merge with develop/3.4

pull/6641/head
Andrii Shvaika 4 years ago
parent
commit
63750ef8a0
  1. 4
      application/pom.xml
  2. 7
      application/src/main/data/upgrade/3.3.4/schema_update.sql
  3. 8
      application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java
  4. 38
      application/src/main/java/org/thingsboard/server/config/JwtSettings.java
  5. 6
      application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java
  6. 6
      application/src/main/java/org/thingsboard/server/controller/AuthController.java
  7. 41
      application/src/main/java/org/thingsboard/server/controller/BaseController.java
  8. 2
      application/src/main/java/org/thingsboard/server/controller/CustomerController.java
  9. 6
      application/src/main/java/org/thingsboard/server/controller/DashboardController.java
  10. 104
      application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java
  11. 62
      application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java
  12. 403
      application/src/main/java/org/thingsboard/server/controller/EntityViewController.java
  13. 272
      application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java
  14. 152
      application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java
  15. 5
      application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java
  16. 24
      application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java
  17. 38
      application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java
  18. 21
      application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java
  19. 8
      application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java
  20. 4
      application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java
  21. 112
      application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/DefaultTbDeviceProfileService.java
  22. 26
      application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/TbDeviceProfileService.java
  23. 76
      application/src/main/java/org/thingsboard/server/service/entitiy/entityRelation/DefaultTbEntityRelationService.java
  24. 33
      application/src/main/java/org/thingsboard/server/service/entitiy/entityRelation/TbEntityRelationService.java
  25. 388
      application/src/main/java/org/thingsboard/server/service/entitiy/entityView/DefaultTbEntityViewService.java
  26. 47
      application/src/main/java/org/thingsboard/server/service/entitiy/entityView/TbEntityViewService.java
  27. 2
      application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java
  28. 21
      application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java
  29. 24
      application/src/main/java/org/thingsboard/server/service/security/auth/MfaAuthenticationToken.java
  30. 190
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/DefaultTwoFactorAuthService.java
  31. 45
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java
  32. 168
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/DefaultTwoFaConfigManager.java
  33. 46
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java
  34. 39
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java
  35. 73
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java
  36. 68
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFaProvider.java
  37. 76
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java
  38. 76
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java
  39. 74
      application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java
  40. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java
  41. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java
  42. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java
  43. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java
  44. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java
  45. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java
  46. 2
      application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java
  47. 88
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java
  48. 41
      application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAwareAuthenticationSuccessHandler.java
  49. 12
      application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java
  50. 12
      application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java
  51. 119
      application/src/main/java/org/thingsboard/server/service/security/model/token/JwtTokenFactory.java
  52. 114
      application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java
  53. 10
      application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java
  54. 6
      application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java
  55. 3
      application/src/main/resources/i18n/messages.properties
  56. 117
      application/src/main/resources/templates/2fa.verification.code.ftl
  57. 2
      application/src/main/resources/templates/account.lockout.ftl
  58. 3
      application/src/main/resources/thingsboard.yml
  59. 9
      application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java
  60. 470
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java
  61. 441
      application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthTest.java
  62. 23
      application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthConfigSqlTest.java
  63. 23
      application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthSqlTest.java
  64. 165
      application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java
  65. 19
      common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java
  66. 1
      common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java
  67. 18
      common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java
  68. 2
      common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java
  69. 26
      common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java
  70. 5
      common/data/src/main/java/org/thingsboard/server/common/data/security/Authority.java
  71. 34
      common/data/src/main/java/org/thingsboard/server/common/data/security/UserAuthSettings.java
  72. 55
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/PlatformTwoFaSettings.java
  73. 26
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java
  74. 57
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/BackupCodeTwoFaAccountConfig.java
  75. 38
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java
  76. 24
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java
  77. 38
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java
  78. 39
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java
  79. 47
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java
  80. 33
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java
  81. 30
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java
  82. 28
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java
  83. 37
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java
  84. 33
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java
  85. 39
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java
  86. 23
      common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java
  87. 8
      common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java
  88. 83
      common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java
  89. 2
      common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java
  90. 5
      common/util/src/main/java/org/thingsboard/common/util/CollectionsUtil.java
  91. 7
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java
  92. 2
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelProperties.java
  93. 1
      dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java
  94. 7
      dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java
  95. 81
      dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java
  96. 4
      dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java
  97. 56
      dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java
  98. 33
      dao/src/main/java/org/thingsboard/server/dao/sql/user/UserAuthSettingsRepository.java
  99. 28
      dao/src/main/java/org/thingsboard/server/dao/user/UserAuthSettingsDao.java
  100. 20
      dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java

4
application/pom.xml

@ -345,6 +345,10 @@
<artifactId>Java-WebSocket</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jboss.aerogear</groupId>
<artifactId>aerogear-otp-java</artifactId>
</dependency>
</dependencies>
<build>

7
application/src/main/data/upgrade/3.3.4/schema_update.sql

@ -44,3 +44,10 @@ CREATE TABLE IF NOT EXISTS queue (
processing_strategy varchar(255),
additional_info varchar
);
CREATE TABLE IF NOT EXISTS user_auth_settings (
id uuid NOT NULL CONSTRAINT user_auth_settings_pkey PRIMARY KEY,
created_time bigint NOT NULL,
user_id uuid UNIQUE NOT NULL CONSTRAINT fk_user_auth_settings_user_id REFERENCES tb_user(id),
two_fa_settings varchar
);

8
application/src/main/java/org/thingsboard/server/actors/ruleChain/DefaultTbContext.java

@ -177,7 +177,9 @@ class DefaultTbContext implements TbContext {
private void enqueue(TopicPartitionInfo tpi, TbMsg tbMsg, Consumer<Throwable> onFailure, Runnable onSuccess) {
if (!tbMsg.isValid()) {
log.trace("[{}] Skip invalid message: {}", getTenantId(), tbMsg);
onFailure.accept(new IllegalArgumentException("Source message is no longer valid!"));
if (onFailure != null) {
onFailure.accept(new IllegalArgumentException("Source message is no longer valid!"));
}
return;
}
TransportProtos.ToRuleEngineMsg msg = TransportProtos.ToRuleEngineMsg.newBuilder()
@ -252,7 +254,9 @@ class DefaultTbContext implements TbContext {
private void enqueueForTellNext(TopicPartitionInfo tpi, QueueId queueId, TbMsg source, Set<String> relationTypes, String failureMessage, Runnable onSuccess, Consumer<Throwable> onFailure) {
if (!source.isValid()) {
log.trace("[{}] Skip invalid message: {}", getTenantId(), source);
onFailure.accept(new IllegalArgumentException("Source message is no longer valid!"));
if (onFailure != null) {
onFailure.accept(new IllegalArgumentException("Source message is no longer valid!"));
}
return;
}
RuleChainId ruleChainId = nodeCtx.getSelf().getRuleChainId();

38
application/src/main/java/org/thingsboard/server/config/JwtSettings.java

@ -15,12 +15,14 @@
*/
package org.thingsboard.server.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtToken;
@Configuration
@Component
@ConfigurationProperties(prefix = "security.jwt")
@Data
public class JwtSettings {
/**
* {@link JwtToken} will expire after this time.
@ -42,34 +44,4 @@ public class JwtSettings {
*/
private Integer refreshTokenExpTime;
public Integer getRefreshTokenExpTime() {
return refreshTokenExpTime;
}
public void setRefreshTokenExpTime(Integer refreshTokenExpTime) {
this.refreshTokenExpTime = refreshTokenExpTime;
}
public Integer getTokenExpirationTime() {
return tokenExpirationTime;
}
public void setTokenExpirationTime(Integer tokenExpirationTime) {
this.tokenExpirationTime = tokenExpirationTime;
}
public String getTokenIssuer() {
return tokenIssuer;
}
public void setTokenIssuer(String tokenIssuer) {
this.tokenIssuer = tokenIssuer;
}
public String getTokenSigningKey() {
return tokenSigningKey;
}
public void setTokenSigningKey(String tokenSigningKey) {
this.tokenSigningKey = tokenSigningKey;
}
}
}

6
application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java

@ -42,6 +42,7 @@ import org.springframework.web.filter.CorsFilter;
import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.exception.ThingsboardErrorResponseHandler;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider;
import org.thingsboard.server.service.security.auth.jwt.JwtTokenAuthenticationProcessingFilter;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider;
@ -61,6 +62,7 @@ import java.util.List;
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled=true)
@Order(SecurityProperties.BASIC_AUTH_ORDER)
@TbCoreComponent
public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapter {
public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization";
@ -241,8 +243,4 @@ public class ThingsboardSecurityConfiguration extends WebSecurityConfigurerAdapt
}
}
@Bean
public AuditLogLevelFilter auditLogLevelFilter(@Autowired AuditLogLevelProperties auditLogLevelProperties) {
return new AuditLogLevelFilter(auditLogLevelProperties.getMask());
}
}

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

@ -82,7 +82,7 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Get current User (getUser)",
notes = "Get the information about the User which credentials are used to perform this REST API call.")
@PreAuthorize("isAuthenticated()")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/user", method = RequestMethod.GET)
public @ResponseBody
User getUser() throws ThingsboardException {
@ -96,7 +96,7 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Logout (logout)",
notes = "Special API call to record the 'logout' of the user to the Audit Logs. Since platform uses [JWT](https://jwt.io/), the actual logout is the procedure of clearing the [JWT](https://jwt.io/) token on the client side. ")
@PreAuthorize("isAuthenticated()")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/logout", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public void logout(HttpServletRequest request) throws ThingsboardException {
@ -105,7 +105,7 @@ public class AuthController extends BaseController {
@ApiOperation(value = "Change password for current User (changePassword)",
notes = "Change the password for the User which credentials are used to perform this REST API call. Be aware that previously generated [JWT](https://jwt.io/) tokens will be still valid until they expire.")
@PreAuthorize("isAuthenticated()")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/auth/changePassword", method = RequestMethod.POST)
@ResponseStatus(value = HttpStatus.OK)
public ObjectNode changePassword(

41
application/src/main/java/org/thingsboard/server/controller/BaseController.java

@ -27,9 +27,11 @@ import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.async.DeferredResult;
import org.thingsboard.server.cluster.TbClusterService;
@ -153,6 +155,7 @@ import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.DEFAULT_PAGE_SIZE;
import static org.thingsboard.server.controller.ControllerConstants.INCORRECT_TENANT_ID;
@ -295,11 +298,37 @@ public abstract class BaseController {
@Getter
protected boolean edgesEnabled;
@ExceptionHandler(Exception.class)
public void handleControllerException(Exception e, HttpServletResponse response) {
ThingsboardException thingsboardException = handleException(e);
if (thingsboardException.getErrorCode() == ThingsboardErrorCode.GENERAL && thingsboardException.getCause() instanceof Exception
&& StringUtils.equals(thingsboardException.getCause().getMessage(), thingsboardException.getMessage())) {
e = (Exception) thingsboardException.getCause();
} else {
e = thingsboardException;
}
errorResponseHandler.handle(e, response);
}
@ExceptionHandler(ThingsboardException.class)
public void handleThingsboardException(ThingsboardException ex, HttpServletResponse response) {
errorResponseHandler.handle(ex, response);
}
/**
* @deprecated Exceptions that are not of {@link ThingsboardException} type
* are now caught and mapped to {@link ThingsboardException} by
* {@link ExceptionHandler} {@link BaseController#handleControllerException(Exception, HttpServletResponse)}
* which basically acts like the following boilerplate:
* {@code
* try {
* someExceptionThrowingMethod();
* } catch (Exception e) {
* throw handleException(e);
* }
* }
* */
@Deprecated
ThingsboardException handleException(Exception exception) {
return handleException(exception, true);
}
@ -326,6 +355,18 @@ public abstract class BaseController {
}
}
/**
* Handles validation error for controller method arguments annotated with @{@link javax.validation.Valid}
* */
@ExceptionHandler(MethodArgumentNotValidException.class)
public void handleValidationError(MethodArgumentNotValidException e, HttpServletResponse response) {
String errorMessage = "Validation error: " + e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
ThingsboardException thingsboardException = new ThingsboardException(errorMessage, ThingsboardErrorCode.BAD_REQUEST_PARAMS);
handleThingsboardException(thingsboardException, response);
}
<T> T checkNotNull(T reference) throws ThingsboardException {
return checkNotNull(reference, "Requested item wasn't found!");
}

2
application/src/main/java/org/thingsboard/server/controller/CustomerController.java

@ -143,7 +143,7 @@ public class CustomerController extends BaseController {
@RequestMapping(value = "/customer", method = RequestMethod.POST)
@ResponseBody
public Customer saveCustomer(@ApiParam(value = "A JSON value representing the customer.") @RequestBody Customer customer) throws ThingsboardException {
customer.setTenantId(getCurrentUser().getTenantId());
customer.setTenantId(getTenantId());
checkEntity(customer.getId(), customer, Resource.CUSTOMER);
return tbCustomerService.save(customer, getCurrentUser());
}

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

@ -181,7 +181,7 @@ public class DashboardController extends BaseController {
public Dashboard saveDashboard(
@ApiParam(value = "A JSON value representing the dashboard.")
@RequestBody Dashboard dashboard) throws ThingsboardException {
dashboard.setTenantId(getCurrentUser().getTenantId());
dashboard.setTenantId(getTenantId());
checkEntity(dashboard.getId(), dashboard, Resource.DASHBOARD);
return tbDashboardService.save(dashboard, getCurrentUser());
}
@ -448,7 +448,7 @@ public class DashboardController extends BaseController {
"If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. "
+ DASHBOARD_DEFINITION + TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("isAuthenticated()")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/home", method = RequestMethod.GET)
@ResponseBody
public HomeDashboard getHomeDashboard() throws ThingsboardException {
@ -485,7 +485,7 @@ public class DashboardController extends BaseController {
"If 'homeDashboardId' parameter is not set on the User and Customer levels then checks the same parameter for the Tenant that owns the user. " +
TENANT_OR_CUSTOMER_AUTHORITY_PARAGRAPH,
produces = MediaType.APPLICATION_JSON_VALUE)
@PreAuthorize("isAuthenticated()")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
@RequestMapping(value = "/dashboard/home/info", method = RequestMethod.GET)
@ResponseBody
public HomeDashboardInfo getHomeDashboardInfo() throws ThingsboardException {

104
application/src/main/java/org/thingsboard/server/controller/DeviceProfileController.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -32,21 +33,17 @@ import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.DeviceProfileInfo;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.deviceProfile.TbDeviceProfileService;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import static org.thingsboard.server.controller.ControllerConstants.DEVICE_PROFILE_DATA;
@ -70,9 +67,11 @@ import static org.thingsboard.server.controller.ControllerConstants.UUID_WIKI_LI
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
@Slf4j
public class DeviceProfileController extends BaseController {
private final TbDeviceProfileService tbDeviceProfileService;
@Autowired
private TimeseriesService timeseriesService;
@ -201,45 +200,9 @@ public class DeviceProfileController extends BaseController {
public DeviceProfile saveDeviceProfile(
@ApiParam(value = "A JSON value representing the device profile.")
@RequestBody DeviceProfile deviceProfile) throws ThingsboardException {
try {
boolean created = deviceProfile.getId() == null;
deviceProfile.setTenantId(getTenantId());
checkEntity(deviceProfile.getId(), deviceProfile, Resource.DEVICE_PROFILE);
boolean isFirmwareChanged = false;
boolean isSoftwareChanged = false;
if (!created) {
DeviceProfile oldDeviceProfile = deviceProfileService.findDeviceProfileById(getTenantId(), deviceProfile.getId());
if (!Objects.equals(deviceProfile.getFirmwareId(), oldDeviceProfile.getFirmwareId())) {
isFirmwareChanged = true;
}
if (!Objects.equals(deviceProfile.getSoftwareId(), oldDeviceProfile.getSoftwareId())) {
isSoftwareChanged = true;
}
}
DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile));
vcService.autoCommit(getCurrentUser(), savedDeviceProfile.getId());
tbClusterService.onDeviceProfileChange(savedDeviceProfile, null);
tbClusterService.broadcastEntityStateChangeEvent(deviceProfile.getTenantId(), savedDeviceProfile.getId(),
created ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
logEntityAction(savedDeviceProfile.getId(), savedDeviceProfile,
null,
created ? ActionType.ADDED : ActionType.UPDATED, null);
otaPackageStateService.update(savedDeviceProfile, isFirmwareChanged, isSoftwareChanged);
sendEntityNotificationMsg(getTenantId(), savedDeviceProfile.getId(),
deviceProfile.getId() == null ? EdgeEventActionType.ADDED : EdgeEventActionType.UPDATED);
return savedDeviceProfile;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE_PROFILE), deviceProfile,
null, deviceProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
deviceProfile.setTenantId(getTenantId());
checkEntity(deviceProfile.getId(), deviceProfile, Resource.DEVICE_PROFILE);
return tbDeviceProfileService.save(deviceProfile, getCurrentUser());
}
@ApiOperation(value = "Delete device profile (deleteDeviceProfile)",
@ -253,27 +216,10 @@ public class DeviceProfileController extends BaseController {
@ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
try {
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE);
deviceProfileService.deleteDeviceProfile(getTenantId(), deviceProfileId);
tbClusterService.onDeviceProfileDelete(deviceProfile, null);
tbClusterService.broadcastEntityStateChangeEvent(deviceProfile.getTenantId(), deviceProfile.getId(), ComponentLifecycleEvent.DELETED);
logEntityAction(deviceProfileId, deviceProfile,
null,
ActionType.DELETED, null, strDeviceProfileId);
sendEntityNotificationMsg(getTenantId(), deviceProfile.getId(), EdgeEventActionType.DELETED);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE_PROFILE),
null,
null,
ActionType.DELETED, e, strDeviceProfileId);
throw handleException(e);
}
}
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.DELETE);
tbDeviceProfileService.delete(deviceProfile, getCurrentUser());
}
@ApiOperation(value = "Make Device Profile Default (setDefaultDeviceProfile)",
notes = "Marks device profile as default within a tenant scope." + TENANT_AUTHORITY_PARAGRAPH,
@ -285,30 +231,10 @@ public class DeviceProfileController extends BaseController {
@ApiParam(value = DEVICE_PROFILE_ID_PARAM_DESCRIPTION)
@PathVariable(DEVICE_PROFILE_ID) String strDeviceProfileId) throws ThingsboardException {
checkParameter(DEVICE_PROFILE_ID, strDeviceProfileId);
try {
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.WRITE);
DeviceProfile previousDefaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(getTenantId());
if (deviceProfileService.setDefaultDeviceProfile(getTenantId(), deviceProfileId)) {
if (previousDefaultDeviceProfile != null) {
previousDefaultDeviceProfile = deviceProfileService.findDeviceProfileById(getTenantId(), previousDefaultDeviceProfile.getId());
logEntityAction(previousDefaultDeviceProfile.getId(), previousDefaultDeviceProfile,
null, ActionType.UPDATED, null);
}
deviceProfile = deviceProfileService.findDeviceProfileById(getTenantId(), deviceProfileId);
logEntityAction(deviceProfile.getId(), deviceProfile,
null, ActionType.UPDATED, null);
}
return deviceProfile;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.DEVICE_PROFILE),
null,
null,
ActionType.UPDATED, e, strDeviceProfileId);
throw handleException(e);
}
DeviceProfileId deviceProfileId = new DeviceProfileId(toUUID(strDeviceProfileId));
DeviceProfile deviceProfile = checkDeviceProfileId(deviceProfileId, Operation.WRITE);
DeviceProfile previousDefaultDeviceProfile = deviceProfileService.findDefaultDeviceProfile(getTenantId());
return tbDeviceProfileService.setDefaultDeviceProfile(deviceProfile, previousDefaultDeviceProfile, getCurrentUser());
}
@ApiOperation(value = "Get Device Profiles (getDeviceProfiles)",

62
application/src/main/java/org/thingsboard/server/controller/EntityRelationController.java

@ -17,6 +17,7 @@ package org.thingsboard.server.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
@ -27,9 +28,6 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityIdFactory;
@ -38,6 +36,7 @@ import org.thingsboard.server.common.data.relation.EntityRelationInfo;
import org.thingsboard.server.common.data.relation.EntityRelationsQuery;
import org.thingsboard.server.common.data.relation.RelationTypeGroup;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.entityRelation.TbEntityRelationService;
import org.thingsboard.server.service.security.permission.Operation;
import java.util.List;
@ -52,8 +51,11 @@ import static org.thingsboard.server.controller.ControllerConstants.RELATION_TYP
@RestController
@TbCoreComponent
@RequestMapping("/api")
@RequiredArgsConstructor
public class EntityRelationController extends BaseController {
private final TbEntityRelationService tbEntityRelationService;
public static final String TO_TYPE = "toType";
public static final String FROM_ID = "fromId";
public static final String FROM_TYPE = "fromType";
@ -77,28 +79,14 @@ public class EntityRelationController extends BaseController {
@ResponseStatus(value = HttpStatus.OK)
public void saveRelation(@ApiParam(value = "A JSON value representing the relation.", required = true)
@RequestBody EntityRelation relation) throws ThingsboardException {
try {
checkNotNull(relation);
checkEntityId(relation.getFrom(), Operation.WRITE);
checkEntityId(relation.getTo(), Operation.WRITE);
if (relation.getTypeGroup() == null) {
relation.setTypeGroup(RelationTypeGroup.COMMON);
}
relationService.saveRelation(getTenantId(), relation);
logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(),
ActionType.RELATION_ADD_OR_UPDATE, null, relation);
logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(),
ActionType.RELATION_ADD_OR_UPDATE, null, relation);
sendRelationNotificationMsg(getTenantId(), relation, EdgeEventActionType.RELATION_ADD_OR_UPDATE);
} catch (Exception e) {
logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(),
ActionType.RELATION_ADD_OR_UPDATE, e, relation);
logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(),
ActionType.RELATION_ADD_OR_UPDATE, e, relation);
throw handleException(e);
checkNotNull(relation);
checkEntityId(relation.getFrom(), Operation.WRITE);
checkEntityId(relation.getTo(), Operation.WRITE);
if (relation.getTypeGroup() == null) {
relation.setTypeGroup(RelationTypeGroup.COMMON);
}
tbEntityRelationService.save(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
}
@ApiOperation(value = "Delete Relation (deleteRelation)",
@ -123,24 +111,8 @@ public class EntityRelationController extends BaseController {
checkEntityId(toId, Operation.WRITE);
RelationTypeGroup relationTypeGroup = parseRelationTypeGroup(strRelationTypeGroup, RelationTypeGroup.COMMON);
EntityRelation relation = new EntityRelation(fromId, toId, strRelationType, relationTypeGroup);
try {
Boolean found = relationService.deleteRelation(getTenantId(), fromId, toId, strRelationType, relationTypeGroup);
if (!found) {
throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
}
logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(),
ActionType.RELATION_DELETED, null, relation);
logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(),
ActionType.RELATION_DELETED, null, relation);
sendRelationNotificationMsg(getTenantId(), relation, EdgeEventActionType.RELATION_DELETED);
} catch (Exception e) {
logEntityAction(relation.getFrom(), null, getCurrentUser().getCustomerId(),
ActionType.RELATION_DELETED, e, relation);
logEntityAction(relation.getTo(), null, getCurrentUser().getCustomerId(),
ActionType.RELATION_DELETED, e, relation);
throw handleException(e);
}
tbEntityRelationService.delete(getTenantId(), getCurrentUser().getCustomerId(), relation, getCurrentUser());
}
@ApiOperation(value = "Delete Relations (deleteRelations)",
@ -155,13 +127,7 @@ public class EntityRelationController extends BaseController {
checkParameter("entityType", strType);
EntityId entityId = EntityIdFactory.getByTypeAndId(strType, strId);
checkEntityId(entityId, Operation.WRITE);
try {
relationService.deleteEntityRelations(getTenantId(), entityId);
logEntityAction(entityId, null, getCurrentUser().getCustomerId(), ActionType.RELATIONS_DELETED, null);
} catch (Exception e) {
logEntityAction(entityId, null, getCurrentUser().getCustomerId(), ActionType.RELATIONS_DELETED, e);
throw handleException(e);
}
tbEntityRelationService.deleteRelations (getTenantId(), getCurrentUser().getCustomerId(), entityId, getCurrentUser());
}
@ApiOperation(value = "Get Relation (getRelation)",

403
application/src/main/java/org/thingsboard/server/controller/EntityViewController.java

@ -15,15 +15,11 @@
*/
package org.thingsboard.server.controller;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
@ -36,45 +32,30 @@ import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntitySubtype;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.EntityViewInfo;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.entityview.EntityViewSearchQuery;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.entityView.TbEntityViewService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.permission.Operation;
import org.thingsboard.server.service.security.permission.Resource;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID;
import static org.thingsboard.server.controller.ControllerConstants.CUSTOMER_ID_PARAM_DESCRIPTION;
import static org.thingsboard.server.controller.ControllerConstants.EDGE_ASSIGN_ASYNC_FIRST_STEP_DESCRIPTION;
@ -104,14 +85,14 @@ import static org.thingsboard.server.controller.EdgeController.EDGE_ID;
*/
@RestController
@TbCoreComponent
@RequiredArgsConstructor
@RequestMapping("/api")
@Slf4j
public class EntityViewController extends BaseController {
public static final String ENTITY_VIEW_ID = "entityViewId";
public final TbEntityViewService tbEntityViewService;
@Autowired
private TimeseriesService tsService;
public static final String ENTITY_VIEW_ID = "entityViewId";
@ApiOperation(value = "Get entity view (getEntityViewById)",
notes = "Fetch the EntityView object based on the provided entity view id. "
@ -159,237 +140,15 @@ public class EntityViewController extends BaseController {
public EntityView saveEntityView(
@ApiParam(value = "A JSON object representing the entity view.")
@RequestBody EntityView entityView) throws ThingsboardException {
try {
entityView.setTenantId(getCurrentUser().getTenantId());
List<ListenableFuture<?>> futures = new ArrayList<>();
if (entityView.getId() == null) {
accessControlService
.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.CREATE, null, entityView);
} else {
EntityView existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE);
if (existingEntityView.getKeys() != null) {
if (existingEntityView.getKeys().getAttributes() != null) {
futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.CLIENT_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), getCurrentUser()));
futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.SERVER_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), getCurrentUser()));
futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.SHARED_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), getCurrentUser()));
}
}
List<String> tsKeys = existingEntityView.getKeys() != null && existingEntityView.getKeys().getTimeseries() != null ?
existingEntityView.getKeys().getTimeseries() : Collections.emptyList();
futures.add(deleteLatestFromEntityView(existingEntityView, tsKeys, getCurrentUser()));
}
EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView));
if (savedEntityView.getKeys() != null) {
if (savedEntityView.getKeys().getAttributes() != null) {
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs(), getCurrentUser()));
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SERVER_SCOPE, savedEntityView.getKeys().getAttributes().getSs(), getCurrentUser()));
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SHARED_SCOPE, savedEntityView.getKeys().getAttributes().getSh(), getCurrentUser()));
}
futures.add(copyLatestFromEntityToEntityView(savedEntityView, getCurrentUser()));
}
for (ListenableFuture<?> future : futures) {
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException("Failed to copy attributes to entity view", e);
}
}
logEntityAction(savedEntityView.getId(), savedEntityView, null,
entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, null);
if (entityView.getId() != null) {
sendEntityNotificationMsg(savedEntityView.getTenantId(), savedEntityView.getId(), EdgeEventActionType.UPDATED);
}
return savedEntityView;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ENTITY_VIEW), entityView, null,
entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED, e);
throw handleException(e);
}
}
private ListenableFuture<Void> deleteLatestFromEntityView(EntityView entityView, List<String> keys, SecurityUser user) {
EntityViewId entityId = entityView.getId();
SettableFuture<Void> resultFuture = SettableFuture.create();
if (keys != null && !keys.isEmpty()) {
tsSubService.deleteLatest(entityView.getTenantId(), entityId, keys, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void tmp) {
try {
logTimeseriesDeleted(user, entityId, keys, null);
} catch (ThingsboardException e) {
log.error("Failed to log timeseries delete", e);
}
resultFuture.set(tmp);
}
@Override
public void onFailure(Throwable t) {
try {
logTimeseriesDeleted(user, entityId, keys, t);
} catch (ThingsboardException e) {
log.error("Failed to log timeseries delete", e);
}
resultFuture.setException(t);
}
});
} else {
tsSubService.deleteAllLatest(entityView.getTenantId(), entityId, new FutureCallback<Collection<String>>() {
@Override
public void onSuccess(@Nullable Collection<String> keys) {
try {
logTimeseriesDeleted(user, entityId, new ArrayList<>(keys), null);
} catch (ThingsboardException e) {
log.error("Failed to log timeseries delete", e);
}
resultFuture.set(null);
}
@Override
public void onFailure(Throwable t) {
try {
logTimeseriesDeleted(user, entityId, Collections.emptyList(), t);
} catch (ThingsboardException e) {
log.error("Failed to log timeseries delete", e);
}
resultFuture.setException(t);
}
});
}
return resultFuture;
}
private ListenableFuture<Void> deleteAttributesFromEntityView(EntityView entityView, String scope, List<String> keys, SecurityUser user) {
EntityViewId entityId = entityView.getId();
SettableFuture<Void> resultFuture = SettableFuture.create();
if (keys != null && !keys.isEmpty()) {
tsSubService.deleteAndNotify(entityView.getTenantId(), entityId, scope, keys, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void tmp) {
try {
logAttributesDeleted(user, entityId, scope, keys, null);
} catch (ThingsboardException e) {
log.error("Failed to log attribute delete", e);
}
resultFuture.set(tmp);
}
@Override
public void onFailure(Throwable t) {
try {
logAttributesDeleted(user, entityId, scope, keys, t);
} catch (ThingsboardException e) {
log.error("Failed to log attribute delete", e);
}
resultFuture.setException(t);
}
});
} else {
resultFuture.set(null);
}
return resultFuture;
}
private ListenableFuture<List<Void>> copyLatestFromEntityToEntityView(EntityView entityView, SecurityUser user) {
EntityViewId entityId = entityView.getId();
List<String> keys = entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null ?
entityView.getKeys().getTimeseries() : Collections.emptyList();
long startTs = entityView.getStartTimeMs();
long endTs = entityView.getEndTimeMs() == 0 ? Long.MAX_VALUE : entityView.getEndTimeMs();
ListenableFuture<List<String>> keysFuture;
if (keys.isEmpty()) {
keysFuture = Futures.transform(tsService.findAllLatest(user.getTenantId(),
entityView.getEntityId()), latest -> latest.stream().map(TsKvEntry::getKey).collect(Collectors.toList()), MoreExecutors.directExecutor());
} else {
keysFuture = Futures.immediateFuture(keys);
}
ListenableFuture<List<TsKvEntry>> latestFuture = Futures.transformAsync(keysFuture, fetchKeys -> {
List<ReadTsKvQuery> queries = fetchKeys.stream().filter(key -> !isBlank(key)).map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, "DESC")).collect(Collectors.toList());
if (!queries.isEmpty()) {
return tsService.findAll(user.getTenantId(), entityView.getEntityId(), queries);
} else {
return Futures.immediateFuture(null);
}
}, MoreExecutors.directExecutor());
return Futures.transform(latestFuture, latestValues -> {
if (latestValues != null && !latestValues.isEmpty()) {
tsSubService.saveLatestAndNotify(entityView.getTenantId(), entityId, latestValues, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void tmp) {
}
@Override
public void onFailure(Throwable t) {
}
});
}
return null;
}, MoreExecutors.directExecutor());
}
private ListenableFuture<List<Void>> copyAttributesFromEntityToEntityView(EntityView entityView, String scope, Collection<String> keys, SecurityUser user) throws ThingsboardException {
EntityViewId entityId = entityView.getId();
if (keys != null && !keys.isEmpty()) {
ListenableFuture<List<AttributeKvEntry>> getAttrFuture = attributesService.find(getTenantId(), entityView.getEntityId(), scope, keys);
return Futures.transform(getAttrFuture, attributeKvEntries -> {
List<AttributeKvEntry> attributes;
if (attributeKvEntries != null && !attributeKvEntries.isEmpty()) {
attributes =
attributeKvEntries.stream()
.filter(attributeKvEntry -> {
long startTime = entityView.getStartTimeMs();
long endTime = entityView.getEndTimeMs();
long lastUpdateTs = attributeKvEntry.getLastUpdateTs();
return startTime == 0 && endTime == 0 ||
(endTime == 0 && startTime < lastUpdateTs) ||
(startTime == 0 && endTime > lastUpdateTs)
? true : startTime < lastUpdateTs && endTime > lastUpdateTs;
}).collect(Collectors.toList());
tsSubService.saveAndNotify(entityView.getTenantId(), entityId, scope, attributes, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void tmp) {
try {
logAttributesUpdated(user, entityId, scope, attributes, null);
} catch (ThingsboardException e) {
log.error("Failed to log attribute updates", e);
}
}
@Override
public void onFailure(Throwable t) {
try {
logAttributesUpdated(user, entityId, scope, attributes, t);
} catch (ThingsboardException e) {
log.error("Failed to log attribute updates", e);
}
}
});
}
return null;
}, MoreExecutors.directExecutor());
entityView.setTenantId(getCurrentUser().getTenantId());
EntityView existingEntityView = null;
if (entityView.getId() == null) {
accessControlService
.checkPermission(getCurrentUser(), Resource.ENTITY_VIEW, Operation.CREATE, null, entityView);
} else {
return Futures.immediateFuture(null);
existingEntityView = checkEntityViewId(entityView.getId(), Operation.WRITE);
}
}
private void logAttributesUpdated(SecurityUser user, EntityId entityId, String scope, List<AttributeKvEntry> attributes, Throwable e) throws ThingsboardException {
logEntityAction(user, entityId, null, null, ActionType.ATTRIBUTES_UPDATED, toException(e),
scope, attributes);
}
private void logAttributesDeleted(SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) throws ThingsboardException {
logEntityAction(user, entityId, null, null, ActionType.ATTRIBUTES_DELETED, toException(e),
scope, keys);
}
private void logTimeseriesDeleted(SecurityUser user, EntityId entityId, List<String> keys, Throwable e) throws ThingsboardException {
logEntityAction(user, entityId, null, null, ActionType.TIMESERIES_DELETED, toException(e),
keys);
return tbEntityViewService.save(entityView, existingEntityView, getCurrentUser());
}
@ApiOperation(value = "Delete entity view (deleteEntityView)",
@ -402,24 +161,9 @@ public class EntityViewController extends BaseController {
@ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
EntityView entityView = checkEntityViewId(entityViewId, Operation.DELETE);
List<EdgeId> relatedEdgeIds = findRelatedEdgeIds(getTenantId(), entityViewId);
entityViewService.deleteEntityView(getTenantId(), entityViewId);
logEntityAction(entityViewId, entityView, entityView.getCustomerId(),
ActionType.DELETED, null, strEntityViewId);
sendDeleteNotificationMsg(getTenantId(), entityViewId, relatedEdgeIds);
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ENTITY_VIEW),
null,
null,
ActionType.DELETED, e, strEntityViewId);
throw handleException(e);
}
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
EntityView entityView = checkEntityViewId(entityViewId, Operation.DELETE);
tbEntityViewService.delete(entityView, getCurrentUser());
}
@ApiOperation(value = "Get Entity View by name (getTenantEntityView)",
@ -451,28 +195,14 @@ public class EntityViewController extends BaseController {
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(CUSTOMER_ID, strCustomerId);
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
Customer customer = checkCustomerId(customerId, Operation.READ);
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
checkEntityViewId(entityViewId, Operation.ASSIGN_TO_CUSTOMER);
EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(getTenantId(), entityViewId, customerId));
logEntityAction(entityViewId, savedEntityView,
savedEntityView.getCustomerId(),
ActionType.ASSIGNED_TO_CUSTOMER, null, strEntityViewId, strCustomerId, customer.getName());
CustomerId customerId = new CustomerId(toUUID(strCustomerId));
Customer customer = checkCustomerId(customerId, Operation.READ);
sendEntityAssignToCustomerNotificationMsg(savedEntityView.getTenantId(), savedEntityView.getId(),
customerId, EdgeEventActionType.ASSIGNED_TO_CUSTOMER);
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
checkEntityViewId(entityViewId, Operation.ASSIGN_TO_CUSTOMER);
return savedEntityView;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ENTITY_VIEW), null,
null,
ActionType.ASSIGNED_TO_CUSTOMER, e, strEntityViewId, strCustomerId);
throw handleException(e);
}
return tbEntityViewService.assignEntityViewToCustomer(getTenantId(), entityViewId, customer, getCurrentUser());
}
@ApiOperation(value = "Unassign Entity View from customer (unassignEntityViewFromCustomer)",
@ -484,28 +214,15 @@ public class EntityViewController extends BaseController {
@ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
EntityView entityView = checkEntityViewId(entityViewId, Operation.UNASSIGN_FROM_CUSTOMER);
if (entityView.getCustomerId() == null || entityView.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
throw new IncorrectParameterException("Entity View isn't assigned to any customer!");
}
Customer customer = checkCustomerId(entityView.getCustomerId(), Operation.READ);
EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromCustomer(getTenantId(), entityViewId));
logEntityAction(entityViewId, entityView,
entityView.getCustomerId(),
ActionType.UNASSIGNED_FROM_CUSTOMER, null, strEntityViewId, customer.getId().toString(), customer.getName());
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
EntityView entityView = checkEntityViewId(entityViewId, Operation.UNASSIGN_FROM_CUSTOMER);
if (entityView.getCustomerId() == null || entityView.getCustomerId().getId().equals(ModelConstants.NULL_UUID)) {
throw new IncorrectParameterException("Entity View isn't assigned to any customer!");
}
sendEntityAssignToCustomerNotificationMsg(savedEntityView.getTenantId(), savedEntityView.getId(),
customer.getId(), EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER);
Customer customer = checkCustomerId(entityView.getCustomerId(), Operation.READ);
return savedEntityView;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ENTITY_VIEW), null,
null,
ActionType.UNASSIGNED_FROM_CUSTOMER, e, strEntityViewId);
throw handleException(e);
}
return tbEntityViewService.unassignEntityViewFromCustomer(getTenantId(), entityViewId, customer, getCurrentUser());
}
@ApiOperation(value = "Get Customer Entity Views (getCustomerEntityViews)",
@ -705,23 +422,13 @@ public class EntityViewController extends BaseController {
@ApiParam(value = ENTITY_VIEW_ID_PARAM_DESCRIPTION)
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
EntityView entityView = checkEntityViewId(entityViewId, Operation.ASSIGN_TO_CUSTOMER);
Customer publicCustomer = customerService.findOrCreatePublicCustomer(entityView.getTenantId());
EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(getCurrentUser().getTenantId(), entityViewId, publicCustomer.getId()));
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
checkEntityViewId(entityViewId, Operation.ASSIGN_TO_CUSTOMER);
logEntityAction(entityViewId, savedEntityView,
savedEntityView.getCustomerId(),
ActionType.ASSIGNED_TO_CUSTOMER, null, strEntityViewId, publicCustomer.getId().toString(), publicCustomer.getName());
Customer publicCustomer = customerService.findOrCreatePublicCustomer(getTenantId());
return savedEntityView;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ENTITY_VIEW), null,
null,
ActionType.ASSIGNED_TO_CUSTOMER, e, strEntityViewId);
throw handleException(e);
}
return tbEntityViewService.assignEntityViewToPublicCustomer(getTenantId(), getCurrentUser().getCustomerId(),
publicCustomer, entityViewId, getCurrentUser());
}
@ApiOperation(value = "Assign entity view to edge (assignEntityViewToEdge)",
@ -738,27 +445,14 @@ public class EntityViewController extends BaseController {
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
Edge edge = checkEdgeId(edgeId, Operation.READ);
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
checkEntityViewId(entityViewId, Operation.READ);
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
Edge edge = checkEdgeId(edgeId, Operation.READ);
EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToEdge(getTenantId(), entityViewId, edgeId));
logEntityAction(entityViewId, savedEntityView,
savedEntityView.getCustomerId(),
ActionType.ASSIGNED_TO_EDGE, null, strEntityViewId, strEdgeId, edge.getName());
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
checkEntityViewId(entityViewId, Operation.READ);
sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedEntityView.getId(), EdgeEventActionType.ASSIGNED_TO_EDGE);
return savedEntityView;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ENTITY_VIEW), null,
null,
ActionType.ASSIGNED_TO_EDGE, e, strEntityViewId, strEdgeId);
throw handleException(e);
}
return tbEntityViewService.assignEntityViewToEdge(getTenantId(), getCurrentUser().getCustomerId(), entityViewId, edge, getCurrentUser());
}
@ApiOperation(value = "Unassign entity view from edge (unassignEntityViewFromEdge)",
@ -775,27 +469,14 @@ public class EntityViewController extends BaseController {
@PathVariable(ENTITY_VIEW_ID) String strEntityViewId) throws ThingsboardException {
checkParameter(EDGE_ID, strEdgeId);
checkParameter(ENTITY_VIEW_ID, strEntityViewId);
try {
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
Edge edge = checkEdgeId(edgeId, Operation.READ);
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
EntityView entityView = checkEntityViewId(entityViewId, Operation.READ);
EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromEdge(getTenantId(), entityViewId, edgeId));
logEntityAction(entityViewId, entityView,
entityView.getCustomerId(),
ActionType.UNASSIGNED_FROM_EDGE, null, strEntityViewId, strEdgeId, edge.getName());
EdgeId edgeId = new EdgeId(toUUID(strEdgeId));
Edge edge = checkEdgeId(edgeId, Operation.READ);
sendEntityAssignToEdgeNotificationMsg(getTenantId(), edgeId, savedEntityView.getId(), EdgeEventActionType.UNASSIGNED_FROM_EDGE);
EntityViewId entityViewId = new EntityViewId(toUUID(strEntityViewId));
EntityView entityView = checkEntityViewId(entityViewId, Operation.READ);
return savedEntityView;
} catch (Exception e) {
logEntityAction(emptyId(EntityType.ENTITY_VIEW), null,
null,
ActionType.UNASSIGNED_FROM_EDGE, e, strEntityViewId, strEdgeId);
throw handleException(e);
}
return tbEntityViewService.unassignEntityViewFromEdge(getTenantId(), entityView.getCustomerId(), entityView, edge, getCurrentUser());
}
@PreAuthorize("hasAnyAuthority('TENANT_ADMIN', 'CUSTOMER_USER')")

272
application/src/main/java/org/thingsboard/server/controller/TwoFaConfigController.java

@ -0,0 +1,272 @@
/**
* Copyright © 2016-2022 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.controller;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.validation.Valid;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
@RestController
@RequestMapping("/api/2fa")
@TbCoreComponent
@RequiredArgsConstructor
public class TwoFaConfigController extends BaseController {
private final TwoFaConfigManager twoFaConfigManager;
private final TwoFactorAuthService twoFactorAuthService;
@ApiOperation(value = "Get account 2FA settings (getAccountTwoFaSettings)",
notes = "Get user's account 2FA configuration. Configuration contains configs for different 2FA providers." + NEW_LINE +
"Example:\n" +
"```\n{\n \"configs\": {\n" +
" \"EMAIL\": {\n \"providerType\": \"EMAIL\",\n \"useByDefault\": true,\n \"email\": \"tenant@thingsboard.org\"\n },\n" +
" \"TOTP\": {\n \"providerType\": \"TOTP\",\n \"useByDefault\": false,\n \"authUrl\": \"otpauth://totp/TB%202FA:tenant@thingsboard.org?issuer=TB+2FA&secret=P6Z2TLYTASOGP6LCJZAD24ETT5DACNNX\"\n },\n" +
" \"SMS\": {\n \"providerType\": \"SMS\",\n \"useByDefault\": false,\n \"phoneNumber\": \"+380501253652\"\n }\n" +
" }\n}\n```" +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@GetMapping("/account/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public AccountTwoFaSettings getAccountTwoFaSettings() throws ThingsboardException {
SecurityUser user = getCurrentUser();
return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId()).orElse(null);
}
@ApiOperation(value = "Generate 2FA account config (generateTwoFaAccountConfig)",
notes = "Generate new 2FA account config template for specified provider type. " + NEW_LINE +
"For TOTP, this will return a corresponding account config template " +
"with a generated OTP auth URL (with new random secret key for each API call) that can be then " +
"converted to a QR code to scan with an authenticator app. Example:\n" +
"```\n{\n" +
" \"providerType\": \"TOTP\",\n" +
" \"useByDefault\": false,\n" +
" \"authUrl\": \"otpauth://totp/TB%202FA:tenant@thingsboard.org?issuer=TB+2FA&secret=PNJDNWJVAK4ZTUYT7RFGPQLXA7XGU7PX\"\n" +
"}\n```" + NEW_LINE +
"For EMAIL, the generated config will contain email from user's account:\n" +
"```\n{\n" +
" \"providerType\": \"EMAIL\",\n" +
" \"useByDefault\": false,\n" +
" \"email\": \"tenant@thingsboard.org\"\n" +
"}\n```" + NEW_LINE +
"For SMS 2FA this method will just return a config with empty/default values as there is nothing to generate/preset:\n" +
"```\n{\n" +
" \"providerType\": \"SMS\",\n" +
" \"useByDefault\": false,\n" +
" \"phoneNumber\": null\n" +
"}\n```" + NEW_LINE +
"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')")
public TwoFaAccountConfig generateTwoFaAccountConfig(@ApiParam(value = "2FA provider type to generate new account config for", defaultValue = "TOTP", required = true)
@RequestParam TwoFaProviderType providerType) throws Exception {
SecurityUser user = getCurrentUser();
return twoFactorAuthService.generateNewAccountConfig(user, providerType);
}
@ApiOperation(value = "Submit 2FA account config (submitTwoFaAccountConfig)",
notes = "Submit 2FA account config to prepare for a future verification. " +
"Basically, this method will send a verification code for a given account config, if this has " +
"sense for a chosen 2FA provider. This code is needed to then verify and save the account config." + NEW_LINE +
"Example of EMAIL 2FA account config:\n" +
"```\n{\n" +
" \"providerType\": \"EMAIL\",\n" +
" \"useByDefault\": true,\n" +
" \"email\": \"separate-email-for-2fa@thingsboard.org\"\n" +
"}\n```" + NEW_LINE +
"Example of SMS 2FA account config:\n" +
"```\n{\n" +
" \"providerType\": \"SMS\",\n" +
" \"useByDefault\": false,\n" +
" \"phoneNumber\": \"+38012312321\"\n" +
"}\n```" + NEW_LINE +
"For TOTP this method does nothing." + NEW_LINE +
"Will throw an error (Bad Request) if submitted account config is not valid, " +
"or if the provider is not configured for usage. " +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PostMapping("/account/config/submit")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public void submitTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig) throws Exception {
SecurityUser user = getCurrentUser();
twoFactorAuthService.prepareVerificationCode(user, accountConfig, false);
}
@ApiOperation(value = "Verify and save 2FA account config (verifyAndSaveTwoFaAccountConfig)",
notes = "Checks the verification code for submitted config, and if it is correct, saves the provided account config. " + NEW_LINE +
"Returns whole account's 2FA settings object.\n" +
"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')")
public AccountTwoFaSettings verifyAndSaveTwoFaAccountConfig(@Valid @RequestBody TwoFaAccountConfig accountConfig,
@RequestParam(required = false) String verificationCode) throws Exception {
SecurityUser user = getCurrentUser();
if (twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig.getProviderType()).isPresent()) {
throw new IllegalArgumentException("2FA provider is already configured");
}
boolean verificationSuccess;
if (accountConfig.getProviderType() != TwoFaProviderType.BACKUP_CODE) {
verificationSuccess = twoFactorAuthService.checkVerificationCode(user, verificationCode, accountConfig, false);
} else {
verificationSuccess = true;
}
if (verificationSuccess) {
return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
} else {
throw new IllegalArgumentException("Verification code is incorrect");
}
}
@ApiOperation(value = "Update 2FA account config (updateTwoFaAccountConfig)", notes =
"Update config for a given provider type. \n" +
"Update request example:\n" +
"```\n{\n \"useByDefault\": true\n}\n```\n" +
"Returns whole account's 2FA settings object.\n" +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@PutMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public AccountTwoFaSettings updateTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType,
@RequestBody TwoFaAccountConfigUpdateRequest updateRequest) throws ThingsboardException {
SecurityUser user = getCurrentUser();
TwoFaAccountConfig accountConfig = twoFaConfigManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType)
.orElseThrow(() -> new IllegalArgumentException("Config for " + providerType + " 2FA provider not found"));
accountConfig.setUseByDefault(updateRequest.isUseByDefault());
return twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
}
@ApiOperation(value = "Delete 2FA account config (deleteTwoFaAccountConfig)", notes =
"Delete 2FA config for a given 2FA provider type. \n" +
"Returns whole account's 2FA settings object.\n" +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER)
@DeleteMapping("/account/config")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public AccountTwoFaSettings deleteTwoFaAccountConfig(@RequestParam TwoFaProviderType providerType) throws ThingsboardException {
SecurityUser user = getCurrentUser();
return twoFaConfigManager.deleteTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType);
}
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
"Get the list of provider types available for user to use (the ones configured by tenant or sysadmin).\n" +
"Example of response:\n" +
"```\n[\n \"TOTP\",\n \"EMAIL\",\n \"SMS\"\n]\n```" +
ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER
)
@GetMapping("/providers")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN', 'CUSTOMER_USER')")
public List<TwoFaProviderType> getAvailableTwoFaProviders() throws ThingsboardException {
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), true)
.map(PlatformTwoFaSettings::getProviders).orElse(Collections.emptyList()).stream()
.map(TwoFaProviderConfig::getProviderType)
.collect(Collectors.toList());
}
@ApiOperation(value = "Get platform 2FA settings (getPlatformTwoFaSettings)",
notes = "Get platform settings for 2FA. The settings are described for savePlatformTwoFaSettings API method. " +
"If 2FA is not configured, then an empty response will be returned." +
ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@GetMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
public PlatformTwoFaSettings getPlatformTwoFaSettings() throws ThingsboardException {
return twoFaConfigManager.getPlatformTwoFaSettings(getTenantId(), false).orElse(null);
}
@ApiOperation(value = "Save platform 2FA settings (savePlatformTwoFaSettings)",
notes = "Save 2FA settings for platform. The settings have following properties:\n" +
"- `providers` - the list of 2FA providers' configs. Users will only be allowed to use 2FA providers from this list. \n\n" +
"- `minVerificationCodeSendPeriod` - minimal period in seconds to wait after verification code send request to send next request. \n" +
"- `verificationCodeCheckRateLimit` - rate limit configuration for verification code checking.\n" +
"The format is standard: 'amountOfRequests:periodInSeconds'. The value of '1:60' would limit verification " +
"code checking requests to one per minute.\n" +
"- `maxVerificationFailuresBeforeUserLockout` - maximum number of verification failures before a user gets disabled.\n" +
"- `totalAllowedTimeForVerification` - total amount of time in seconds allotted for verification. " +
"Basically, this property sets a lifetime for pre-verification token. If not set, default value of 30 minutes is used.\n" + NEW_LINE +
"TOTP 2FA provider config has following settings:\n" +
"- `issuerName` - issuer name that will be displayed in an authenticator app near a username. Must not be blank.\n\n" +
"For SMS 2FA provider:\n" +
"- `smsVerificationMessageTemplate` - verification message template. Available template variables " +
"are ${code} and ${userEmail}. It must not be blank and must contain verification code variable.\n" +
"- `verificationCodeLifetime` - verification code lifetime in seconds. Required to be positive.\n\n" +
"For EMAIL provider type:\n" +
"- `verificationCodeLifetime` - the same as for SMS." + NEW_LINE +
"Example of the settings:\n" +
"```\n{\n" +
" \"providers\": [\n" +
" {\n" +
" \"providerType\": \"TOTP\",\n" +
" \"issuerName\": \"TB\"\n" +
" },\n" +
" {\n" +
" \"providerType\": \"EMAIL\",\n" +
" \"verificationCodeLifetime\": 60\n" +
" },\n" +
" {\n" +
" \"providerType\": \"SMS\",\n" +
" \"verificationCodeLifetime\": 60,\n" +
" \"smsVerificationMessageTemplate\": \"Here is your verification code: ${code}\"\n" +
" }\n" +
" ],\n" +
" \"minVerificationCodeSendPeriod\": 60,\n" +
" \"verificationCodeCheckRateLimit\": \"3:900\",\n" +
" \"maxVerificationFailuresBeforeUserLockout\": 10,\n" +
" \"totalAllowedTimeForVerification\": 600\n" +
"}\n```" +
ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH)
@PostMapping("/settings")
@PreAuthorize("hasAnyAuthority('SYS_ADMIN')")
public PlatformTwoFaSettings savePlatformTwoFaSettings(@ApiParam(value = "Settings value", required = true)
@RequestBody PlatformTwoFaSettings twoFaSettings) throws ThingsboardException {
return twoFaConfigManager.savePlatformTwoFaSettings(getTenantId(), twoFaSettings);
}
@Data
public static class TwoFaAccountConfigUpdateRequest {
private boolean useByDefault;
}
}

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

@ -0,0 +1,152 @@
/**
* Copyright © 2016-2022 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.controller;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
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.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.EmailTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import javax.servlet.http.HttpServletRequest;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.thingsboard.server.controller.ControllerConstants.NEW_LINE;
@RestController
@RequestMapping("/api/auth/2fa")
@TbCoreComponent
@RequiredArgsConstructor
public class TwoFactorAuthController extends BaseController {
private final TwoFactorAuthService twoFactorAuthService;
private final TwoFaConfigManager twoFaConfigManager;
private final JwtTokenFactory tokenFactory;
private final SystemSecurityService systemSecurityService;
private final UserService userService;
@ApiOperation(value = "Request 2FA verification code (requestTwoFaVerificationCode)",
notes = "Request 2FA verification code." + NEW_LINE +
"To make a request to this endpoint, you need an access token with the scope of PRE_VERIFICATION_TOKEN, " +
"which is issued on username/password auth if 2FA is enabled." + NEW_LINE +
"The API method is rate limited (using rate limit config from TwoFactorAuthSettings). " +
"Will return a Bad Request error if provider is not configured for usage, " +
"and Too Many Requests error if rate limits are exceeded.")
@PostMapping("/verification/send")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public void requestTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType) throws Exception {
SecurityUser user = getCurrentUser();
twoFactorAuthService.prepareVerificationCode(user, providerType, true);
}
@ApiOperation(value = "Check 2FA verification code (checkTwoFaVerificationCode)",
notes = "Checks 2FA verification code, and if it is correct the method returns a regular access and refresh token pair." + NEW_LINE +
"The API method is rate limited (using rate limit config from TwoFactorAuthSettings), and also will block a user " +
"after X unsuccessful verification attempts if such behavior is configured (in TwoFactorAuthSettings)." + NEW_LINE +
"Will return a Bad Request error if provider is not configured for usage, " +
"and Too Many Requests error if rate limits are exceeded.")
@PostMapping("/verification/check")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public JwtTokenPair checkTwoFaVerificationCode(@RequestParam TwoFaProviderType providerType,
@RequestParam String verificationCode, HttpServletRequest servletRequest) throws Exception {
SecurityUser user = getCurrentUser();
boolean verificationSuccess = twoFactorAuthService.checkVerificationCode(user, providerType, verificationCode, true);
if (verificationSuccess) {
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);
} else {
ThingsboardException error = new ThingsboardException("Verification code is incorrect", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
systemSecurityService.logLoginAction(user, new RestAuthenticationDetails(servletRequest), ActionType.LOGIN, error);
throw error;
}
}
@ApiOperation(value = "Get available 2FA providers (getAvailableTwoFaProviders)", notes =
"Get the list of 2FA provider infos available for user to use. Example:\n" +
"```\n[\n" +
" {\n \"type\": \"EMAIL\",\n \"default\": true,\n \"contact\": \"ab*****ko@gmail.com\"\n },\n" +
" {\n \"type\": \"TOTP\",\n \"default\": false,\n \"contact\": null\n },\n" +
" {\n \"type\": \"SMS\",\n \"default\": false,\n \"contact\": \"+38********12\"\n }\n" +
"]\n```")
@GetMapping("/providers")
@PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')")
public List<TwoFaProviderInfo> getAvailableTwoFaProviders() throws ThingsboardException {
SecurityUser user = getCurrentUser();
Optional<PlatformTwoFaSettings> platformTwoFaSettings = twoFaConfigManager.getPlatformTwoFaSettings(user.getTenantId(), true);
return twoFaConfigManager.getAccountTwoFaSettings(user.getTenantId(), user.getId())
.map(settings -> settings.getConfigs().values()).orElse(Collections.emptyList())
.stream().map(config -> {
String contact = null;
switch (config.getProviderType()) {
case SMS:
String phoneNumber = ((SmsTwoFaAccountConfig) config).getPhoneNumber();
contact = StringUtils.obfuscate(phoneNumber, 2, '*', phoneNumber.indexOf('+') + 1, phoneNumber.length());
break;
case EMAIL:
String email = ((EmailTwoFaAccountConfig) config).getEmail();
contact = StringUtils.obfuscate(email, 2, '*', 0, email.indexOf('@'));
break;
}
return TwoFaProviderInfo.builder()
.type(config.getProviderType())
.isDefault(config.isUseByDefault())
.contact(contact)
.minVerificationCodeSendPeriod(platformTwoFaSettings.get().getMinVerificationCodeSendPeriod())
.build();
})
.collect(Collectors.toList());
}
@Data
@AllArgsConstructor
@Builder
public static class TwoFaProviderInfo {
private TwoFaProviderType type;
private boolean isDefault;
private String contact;
private Integer minVerificationCodeSendPeriod;
}
}

5
application/src/main/java/org/thingsboard/server/install/ThingsboardInstallConfiguration.java

@ -19,6 +19,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.thingsboard.server.dao.audit.AuditLogLevelFilter;
import org.thingsboard.server.dao.audit.AuditLogLevelProperties;
import java.util.HashMap;
@ -28,6 +29,8 @@ public class ThingsboardInstallConfiguration {
@Bean
public AuditLogLevelFilter emptyAuditLogLevelFilter() {
return new AuditLogLevelFilter(new HashMap<>());
var props = new AuditLogLevelProperties();
props.setMask(new HashMap<>());
return new AuditLogLevelFilter(props);
}
}

24
application/src/main/java/org/thingsboard/server/service/entitiy/AbstractTbEntityService.java

@ -21,6 +21,7 @@ import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.User;
@ -40,16 +41,20 @@ import org.thingsboard.server.common.data.page.PageDataIterableByTenantIdEntityI
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.dao.alarm.AlarmService;
import org.thingsboard.server.dao.asset.AssetService;
import org.thingsboard.server.dao.attributes.AttributesService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.dashboard.DashboardService;
import org.thingsboard.server.dao.device.ClaimDevicesService;
import org.thingsboard.server.dao.device.DeviceCredentialsService;
import org.thingsboard.server.dao.device.DeviceProfileService;
import org.thingsboard.server.dao.device.DeviceService;
import org.thingsboard.server.dao.edge.EdgeService;
import org.thingsboard.server.dao.entityview.EntityViewService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.queue.QueueService;
import org.thingsboard.server.dao.relation.RelationService;
import org.thingsboard.server.dao.rule.RuleChainService;
import org.thingsboard.server.dao.tenant.TbTenantProfileCache;
import org.thingsboard.server.dao.tenant.TenantService;
@ -57,6 +62,9 @@ import org.thingsboard.server.service.action.EntityActionService;
import org.thingsboard.server.service.edge.EdgeNotificationService;
import org.thingsboard.server.service.executors.DbCallbackExecutorService;
import org.thingsboard.server.service.sync.vc.EntitiesVersionControlService;
import org.thingsboard.server.service.ota.OtaPackageStateService;
import org.thingsboard.server.service.security.permission.AccessControlService;
import org.thingsboard.server.service.telemetry.TelemetrySubscriptionService;
import javax.mail.MessagingException;
import java.util.ArrayList;
@ -111,6 +119,22 @@ public abstract class AbstractTbEntityService {
protected DashboardService dashboardService;
@Autowired
protected EntitiesVersionControlService vcService;
@Autowired
protected EntityViewService entityViewService;
@Autowired
protected TelemetrySubscriptionService tsSubService;
@Autowired
protected AttributesService attributesService;
@Autowired
protected AccessControlService accessControlService;
@Autowired
protected DeviceProfileService deviceProfileService;
@Autowired
protected TbClusterService tbClusterService;
@Autowired
protected OtaPackageStateService otaPackageStateService;
@Autowired
protected RelationService relationService;
protected ListenableFuture<Void> removeAlarmsByEntityId(TenantId tenantId, EntityId entityId) {
ListenableFuture<PageData<AlarmInfo>> alarmsFuture =

38
application/src/main/java/org/thingsboard/server/service/entitiy/DefaultTbNotificationEntityService.java

@ -24,6 +24,7 @@ import org.thingsboard.rule.engine.api.msg.DeviceCredentialsUpdateNotificationMs
import org.thingsboard.server.cluster.TbClusterService;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.Device;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.HasName;
import org.thingsboard.server.common.data.Tenant;
import org.thingsboard.server.common.data.alarm.Alarm;
@ -37,6 +38,7 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.common.msg.TbMsg;
import org.thingsboard.server.common.msg.TbMsgDataType;
@ -202,6 +204,37 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS
sendEntityNotificationMsg(alarm.getTenantId(), alarm.getId(), edgeTypeByActionType(actionType));
}
@Override
public <E extends HasName, I extends EntityId> void notifyCreateOrUpdateOrDelete(TenantId tenantId, CustomerId customerId,
I entityId, E entity, SecurityUser user,
ActionType actionType, Exception e,
Object... additionalInfo) {
notifyEntity(tenantId, entityId, entity, customerId, actionType, user, e, additionalInfo);
if (e == null) {
sendEntityNotificationMsg(tenantId, entityId, edgeTypeByActionType(actionType));
}
}
@Override
public void notifyCreateOrUpdateOrDeleteRelation(TenantId tenantId, CustomerId customerId,
EntityRelation relation, SecurityUser user,
ActionType actionType, Exception e,
Object... additionalInfo) {
notifyEntity(tenantId, relation.getFrom(), null, customerId, actionType, user, e, additionalInfo);
notifyEntity(tenantId, relation.getTo(), null, customerId, actionType, user, e, additionalInfo);
if (e == null) {
try {
if (!relation.getFrom().getEntityType().equals(EntityType.EDGE) &&
!relation.getTo().getEntityType().equals(EntityType.EDGE)) {
sendNotificationMsgToEdgeService(tenantId, null, null, json.writeValueAsString(relation),
EdgeEventType.RELATION, edgeTypeByActionType(actionType));
}
} catch (Exception e1) {
log.warn("Failed to push relation to core: {}", relation, e1);
}
}
}
private <E extends HasName, I extends EntityId> void logEntityAction(TenantId tenantId, I entityId, E entity, CustomerId customerId,
ActionType actionType, SecurityUser user, Object... additionalInfo) {
logEntityAction(tenantId, entityId, entity, customerId, actionType, user, null, additionalInfo);
@ -297,9 +330,12 @@ public class DefaultTbNotificationEntityService implements TbNotificationEntityS
return EdgeEventActionType.ALARM_CLEAR;
case DELETED:
return EdgeEventActionType.DELETED;
case RELATION_ADD_OR_UPDATE:
return EdgeEventActionType.RELATION_ADD_OR_UPDATE;
case RELATION_DELETED:
return EdgeEventActionType.RELATION_DELETED;
default:
return null;
}
}
}

21
application/src/main/java/org/thingsboard/server/service/entitiy/TbNotificationEntityService.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.common.data.security.DeviceCredentials;
import org.thingsboard.server.service.security.model.SecurityUser;
@ -49,9 +50,9 @@ public interface TbNotificationEntityService {
SecurityUser user, Object... additionalInfo);
void notifyDeleteAlarm(TenantId tenantId, Alarm alarm, EntityId originatorId,
CustomerId customerId, ActionType actionType,
List<EdgeId> relatedEdgeIds,
SecurityUser user, String body, Object... additionalInfo);
CustomerId customerId, ActionType actionType,
List<EdgeId> relatedEdgeIds,
SecurityUser user, String body, Object... additionalInfo);
<E extends HasName, I extends EntityId> void notifyAssignOrUnassignEntityToCustomer(TenantId tenantId, I entityId,
CustomerId customerId, E entity,
@ -68,6 +69,7 @@ public interface TbNotificationEntityService {
void notifyCreateOruUpdateTenant(Tenant tenant, ComponentLifecycleEvent event);
void notifyDeleteTenant(Tenant tenant);
void notifyCreateOrUpdateDevice(TenantId tenantId, DeviceId deviceId, CustomerId customerId, Device device,
@ -82,7 +84,18 @@ public interface TbNotificationEntityService {
void notifyAssignDeviceToTenant(TenantId tenantId, TenantId newTenantId, DeviceId deviceId, CustomerId customerId,
Device device, Tenant tenant, SecurityUser user, Object... additionalInfo);
void notifyEdge(TenantId tenantId, EdgeId edgeId, CustomerId customerId, Edge edge, ActionType actionType, SecurityUser user, Object... additionalInfo);
void notifyEdge(TenantId tenantId, EdgeId edgeId, CustomerId customerId, Edge edge, ActionType actionType,
SecurityUser user, Object... additionalInfo);
void notifyCreateOrUpdateAlarm(Alarm alarm, ActionType actionType, SecurityUser user, Object... additionalInfo);
<E extends HasName, I extends EntityId> void notifyCreateOrUpdateOrDelete(TenantId tenantId, CustomerId customerId,
I entityId, E entity, SecurityUser user,
ActionType actionType, Exception e,
Object... additionalInfo);
void notifyCreateOrUpdateOrDeleteRelation(TenantId tenantId, CustomerId customerId,
EntityRelation relation, SecurityUser user,
ActionType actionType, Exception e,
Object... additionalInfo);
}

8
application/src/main/java/org/thingsboard/server/service/entitiy/customer/DefaultTbCustomerService.java

@ -60,14 +60,14 @@ public class DefaultTbCustomerService extends AbstractTbEntityService implements
TenantId tenantId = customer.getTenantId();
CustomerId customerId = customer.getId();
try {
List<EdgeId> relatedEdgeIds = findRelatedEdgeIds(tenantId, customerId);
List<EdgeId> relatedEdgeIds = findRelatedEdgeIds(tenantId, customer.getId());
customerService.deleteCustomer(tenantId, customerId);
notificationEntityService.notifyDeleteEntity(tenantId, customerId, customer, customerId,
notificationEntityService.notifyDeleteEntity(tenantId, customer.getId(), customer, customerId,
ActionType.DELETED, relatedEdgeIds, user, customerId.toString());
tbClusterService.broadcastEntityStateChangeEvent(tenantId, customerId, ComponentLifecycleEvent.DELETED);
tbClusterService.broadcastEntityStateChangeEvent(tenantId, customer.getId(), ComponentLifecycleEvent.DELETED);
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.CUSTOMER), null, null,
ActionType.DELETED, user, e, customerId.toString());
ActionType.DELETED, user, e, customer.getId().toString());
throw handleException(e);
}
}

4
application/src/main/java/org/thingsboard/server/service/entitiy/dashboard/DefaultTbDashboardService.java

@ -60,12 +60,12 @@ public class DefaultTbDashboardService extends AbstractTbEntityService implement
@Override
public void delete(Dashboard dashboard, SecurityUser user) throws ThingsboardException {
TenantId tenantId = dashboard.getTenantId();
DashboardId dashboardId = dashboard.getId();
TenantId tenantId = dashboard.getTenantId();
try {
List<EdgeId> relatedEdgeIds = findRelatedEdgeIds(tenantId, dashboardId);
dashboardService.deleteDashboard(tenantId, dashboardId);
notificationEntityService.notifyDeleteEntity(tenantId, dashboardId, dashboard, user.getCustomerId(),
notificationEntityService.notifyDeleteEntity(tenantId, dashboardId, dashboard, null,
ActionType.DELETED, relatedEdgeIds, user, dashboardId.toString());
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DASHBOARD), null, null,

112
application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/DefaultTbDeviceProfileService.java

@ -0,0 +1,112 @@
/**
* Copyright © 2016-2022 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.service.entitiy.deviceProfile;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.DeviceProfileId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.plugin.ComponentLifecycleEvent;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Objects;
@Service
@TbCoreComponent
@AllArgsConstructor
@Slf4j
public class DefaultTbDeviceProfileService extends AbstractTbEntityService implements TbDeviceProfileService {
@Override
public DeviceProfile save(DeviceProfile deviceProfile, SecurityUser user) throws ThingsboardException {
ActionType actionType = deviceProfile.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
TenantId tenantId = deviceProfile.getTenantId();
try {
boolean isFirmwareChanged = false;
boolean isSoftwareChanged = false;
if (actionType.equals(ActionType.UPDATED)) {
DeviceProfile oldDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfile.getId());
if (!Objects.equals(deviceProfile.getFirmwareId(), oldDeviceProfile.getFirmwareId())) {
isFirmwareChanged = true;
}
if (!Objects.equals(deviceProfile.getSoftwareId(), oldDeviceProfile.getSoftwareId())) {
isSoftwareChanged = true;
}
}
DeviceProfile savedDeviceProfile = checkNotNull(deviceProfileService.saveDeviceProfile(deviceProfile));
vcService.autoCommit(user, savedDeviceProfile.getId());
tbClusterService.onDeviceProfileChange(savedDeviceProfile, null);
tbClusterService.broadcastEntityStateChangeEvent(tenantId, savedDeviceProfile.getId(),
actionType.equals(ActionType.ADDED) ? ComponentLifecycleEvent.CREATED : ComponentLifecycleEvent.UPDATED);
otaPackageStateService.update(savedDeviceProfile, isFirmwareChanged, isSoftwareChanged);
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, savedDeviceProfile.getId(), savedDeviceProfile, user, actionType, null);
return savedDeviceProfile;
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE_PROFILE), deviceProfile, null,
actionType, user, e);
throw handleException(e);
}
}
@Override
public void delete(DeviceProfile deviceProfile, SecurityUser user) throws ThingsboardException {
DeviceProfileId deviceProfileId = deviceProfile.getId();
TenantId tenantId = deviceProfile.getTenantId();
try {
deviceProfileService.deleteDeviceProfile(tenantId, deviceProfileId);
tbClusterService.onDeviceProfileDelete(deviceProfile, null);
tbClusterService.broadcastEntityStateChangeEvent(tenantId, deviceProfileId, ComponentLifecycleEvent.DELETED);
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, deviceProfileId, deviceProfile, user, ActionType.DELETED, null, deviceProfileId.toString());
} catch (Exception e) {
notificationEntityService.notifyCreateOrUpdateOrDelete(tenantId, null, emptyId(EntityType.DEVICE_PROFILE), null, user, ActionType.DELETED, e, deviceProfileId.toString());
throw handleException(e);
}
}
@Override
public DeviceProfile setDefaultDeviceProfile(DeviceProfile deviceProfile, DeviceProfile previousDefaultDeviceProfile, SecurityUser user) throws ThingsboardException {
TenantId tenantId = deviceProfile.getTenantId();
try {
if (deviceProfileService.setDefaultDeviceProfile(tenantId, deviceProfile.getId())) {
if (previousDefaultDeviceProfile != null) {
previousDefaultDeviceProfile = deviceProfileService.findDeviceProfileById(tenantId, previousDefaultDeviceProfile.getId());
notificationEntityService.notifyEntity(tenantId, previousDefaultDeviceProfile.getId(), previousDefaultDeviceProfile, null,
ActionType.UPDATED, user, null);
}
deviceProfile = deviceProfileService.findDeviceProfileById(tenantId, deviceProfile.getId());
notificationEntityService.notifyEntity(tenantId, deviceProfile.getId(), deviceProfile, null,
ActionType.UPDATED, user, null);
}
return deviceProfile;
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE_PROFILE), null, null,
ActionType.UPDATED, user, e, deviceProfile.getId().toString());
throw handleException(e);
}
}
}

26
application/src/main/java/org/thingsboard/server/service/entitiy/deviceProfile/TbDeviceProfileService.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2022 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.service.entitiy.deviceProfile;
import org.thingsboard.server.common.data.DeviceProfile;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.service.entitiy.SimpleTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TbDeviceProfileService extends SimpleTbEntityService<DeviceProfile> {
DeviceProfile setDefaultDeviceProfile(DeviceProfile deviceProfile, DeviceProfile previousDefaultDeviceProfile, SecurityUser user) throws ThingsboardException;
}

76
application/src/main/java/org/thingsboard/server/service/entitiy/entityRelation/DefaultTbEntityRelationService.java

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2022 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.service.entitiy.entityRelation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
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.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
@Service
@TbCoreComponent
@AllArgsConstructor
@Slf4j
public class DefaultTbEntityRelationService extends AbstractTbEntityService implements TbEntityRelationService {
@Override
public void save(TenantId tenantId, CustomerId customerId, EntityRelation relation, SecurityUser user) throws ThingsboardException {
try {
relationService.saveRelation(tenantId, relation);
notificationEntityService.notifyCreateOrUpdateOrDeleteRelation (tenantId, customerId,
relation, user, ActionType.RELATION_ADD_OR_UPDATE, null, relation);
} catch (Exception e) {
notificationEntityService.notifyCreateOrUpdateOrDeleteRelation (tenantId, customerId,
relation, user, ActionType.RELATION_ADD_OR_UPDATE, e, relation);
throw handleException(e);
}
}
@Override
public void delete(TenantId tenantId, CustomerId customerId, EntityRelation relation, SecurityUser user) throws ThingsboardException {
try {
Boolean found = relationService.deleteRelation(tenantId, relation.getFrom(), relation.getTo(), relation.getType(), relation.getTypeGroup());
if (!found) {
throw new ThingsboardException("Requested item wasn't found!", ThingsboardErrorCode.ITEM_NOT_FOUND);
}
notificationEntityService.notifyCreateOrUpdateOrDeleteRelation (tenantId, customerId,
relation, user, ActionType.RELATION_DELETED, null, relation);
} catch (Exception e) {
notificationEntityService.notifyCreateOrUpdateOrDeleteRelation (tenantId, customerId,
relation, user, ActionType.RELATION_DELETED, e, relation);
throw handleException(e);
}
}
@Override
public void deleteRelations(TenantId tenantId, CustomerId customerId, EntityId entityId, SecurityUser user) throws ThingsboardException {
try {
relationService.deleteEntityRelations(tenantId, entityId);
notificationEntityService.notifyEntity(tenantId, entityId, null, customerId, ActionType.RELATIONS_DELETED, user, null);
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, entityId, null, customerId, ActionType.RELATIONS_DELETED, user, e);
throw handleException(e);
}
}
}

33
application/src/main/java/org/thingsboard/server/service/entitiy/entityRelation/TbEntityRelationService.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2022 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.service.entitiy.entityRelation;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.relation.EntityRelation;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TbEntityRelationService {
void save(TenantId tenantId, CustomerId customerId, EntityRelation entity, SecurityUser user) throws ThingsboardException;
void delete (TenantId tenantId, CustomerId customerId, EntityRelation entity, SecurityUser user) throws ThingsboardException;
void deleteRelations (TenantId tenantId, CustomerId customerId, EntityId entityId, SecurityUser user) throws ThingsboardException;
}

388
application/src/main/java/org/thingsboard/server/service/entitiy/entityView/DefaultTbEntityViewService.java

@ -0,0 +1,388 @@
/**
* Copyright © 2016-2022 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.service.entitiy.entityView;
import com.google.common.util.concurrent.FutureCallback;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.DataConstants;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.edge.EdgeEventActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EdgeId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.kv.AttributeKvEntry;
import org.thingsboard.server.common.data.kv.BaseReadTsKvQuery;
import org.thingsboard.server.common.data.kv.ReadTsKvQuery;
import org.thingsboard.server.common.data.kv.TsKvEntry;
import org.thingsboard.server.dao.timeseries.TimeseriesService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.entitiy.AbstractTbEntityService;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import static org.apache.commons.lang3.StringUtils.isBlank;
@Service
@TbCoreComponent
@AllArgsConstructor
@Slf4j
public class DefaultTbEntityViewService extends AbstractTbEntityService implements TbEntityViewService {
private final TimeseriesService tsService;
@Override
public EntityView save(EntityView entityView, EntityView existingEntityView, SecurityUser user) throws ThingsboardException {
ActionType actionType = entityView.getId() == null ? ActionType.ADDED : ActionType.UPDATED;
try {
List<ListenableFuture<?>> futures = new ArrayList<>();
if (existingEntityView != null) {
if (existingEntityView.getKeys() != null && existingEntityView.getKeys().getAttributes() != null) {
futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.CLIENT_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), user));
futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.SERVER_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), user));
futures.add(deleteAttributesFromEntityView(existingEntityView, DataConstants.SHARED_SCOPE, existingEntityView.getKeys().getAttributes().getCs(), user));
}
List<String> tsKeys = existingEntityView.getKeys() != null && existingEntityView.getKeys().getTimeseries() != null ?
existingEntityView.getKeys().getTimeseries() : Collections.emptyList();
futures.add(deleteLatestFromEntityView(existingEntityView, tsKeys, user));
}
EntityView savedEntityView = checkNotNull(entityViewService.saveEntityView(entityView));
if (savedEntityView.getKeys() != null) {
if (savedEntityView.getKeys().getAttributes() != null) {
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.CLIENT_SCOPE, savedEntityView.getKeys().getAttributes().getCs(), user));
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SERVER_SCOPE, savedEntityView.getKeys().getAttributes().getSs(), user));
futures.add(copyAttributesFromEntityToEntityView(savedEntityView, DataConstants.SHARED_SCOPE, savedEntityView.getKeys().getAttributes().getSh(), user));
}
futures.add(copyLatestFromEntityToEntityView(savedEntityView, user));
}
for (ListenableFuture<?> future : futures) {
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException("Failed to copy attributes to entity view", e);
}
}
notificationEntityService.notifyCreateOrUpdateEntity(savedEntityView.getTenantId(), savedEntityView.getId(), savedEntityView,
null, actionType, user);
return savedEntityView;
} catch (Exception e) {
notificationEntityService.notifyEntity(user.getTenantId(), emptyId(EntityType.ENTITY_VIEW), entityView, null, actionType, user, e);
throw handleException(e);
}
}
@Override
public void delete(EntityView entityView, SecurityUser user) throws ThingsboardException {
TenantId tenantId = entityView.getTenantId();
EntityViewId entityViewId = entityView.getId();
try {
List<EdgeId> relatedEdgeIds = findRelatedEdgeIds(tenantId, entityViewId);
entityViewService.deleteEntityView(tenantId, entityViewId);
notificationEntityService.notifyDeleteEntity(tenantId, entityViewId, entityView, user != null ? user.getCustomerId() : null, ActionType.DELETED,
relatedEdgeIds, user, entityViewId.toString());
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ENTITY_VIEW), null, null,
ActionType.DELETED, user, e, entityViewId.toString());
throw handleException(e);
}
}
@Override
public EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer, SecurityUser user) throws ThingsboardException {
ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER;
CustomerId customerId = customer.getId();
try {
EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(tenantId, entityViewId, customerId));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customerId, savedEntityView,
actionType, EdgeEventActionType.ASSIGNED_TO_CUSTOMER, user, true, customerId.toString(), customer.getName());
return savedEntityView;
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ENTITY_VIEW), null, null,
actionType, user, e, entityViewId.toString(), customerId.toString());
throw handleException(e);
}
}
@Override
public EntityView assignEntityViewToPublicCustomer(TenantId tenantId, CustomerId customerId, Customer publicCustomer,
EntityViewId entityViewId, SecurityUser user) throws ThingsboardException {
ActionType actionType = ActionType.ASSIGNED_TO_CUSTOMER;
try {
EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToCustomer(tenantId,
entityViewId, publicCustomer.getId()));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customerId, savedEntityView,
actionType, null, user, false, savedEntityView.getEntityId().toString(),
publicCustomer.getId().toString(), publicCustomer.getName());
return savedEntityView;
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ENTITY_VIEW), null, null,
actionType, user, e, entityViewId.toString());
throw handleException(e);
}
}
@Override
public EntityView assignEntityViewToEdge(TenantId tenantId, CustomerId customerId, EntityViewId entityViewId, Edge edge, SecurityUser user) throws ThingsboardException {
ActionType actionType = ActionType.ASSIGNED_TO_EDGE;
EdgeId edgeId = edge.getId();
EntityView savedEntityView = checkNotNull(entityViewService.assignEntityViewToEdge(tenantId, entityViewId, edgeId));
try {
notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, entityViewId, customerId,
edgeId, savedEntityView, actionType, EdgeEventActionType.ASSIGNED_TO_EDGE, user, savedEntityView.getEntityId().toString(),
edgeId.toString(), edge.getName());
return savedEntityView;
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.DEVICE), null, null,
actionType, user, e, entityViewId.toString(), edgeId.toString());
throw handleException(e);
}
}
@Override
public EntityView unassignEntityViewFromEdge(TenantId tenantId, CustomerId customerId, EntityView entityView,
Edge edge, SecurityUser user) throws ThingsboardException {
ActionType actionType = ActionType.UNASSIGNED_FROM_EDGE;
EntityViewId entityViewId = entityView.getId();
EdgeId edgeId = edge.getId();
try {
EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromEdge(tenantId, entityViewId, edgeId));
notificationEntityService.notifyAssignOrUnassignEntityToEdge(tenantId, entityViewId, customerId,
edgeId, entityView, actionType, EdgeEventActionType.UNASSIGNED_FROM_EDGE, user, entityViewId.toString(),
edgeId.toString(), edge.getName());
return savedEntityView;
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ENTITY_VIEW), null, null,
actionType, user, e, entityViewId.toString(), edgeId.toString());
throw handleException(e);
}
}
@Override
public EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer, SecurityUser user) throws ThingsboardException {
ActionType actionType = ActionType.UNASSIGNED_FROM_CUSTOMER;
try {
EntityView savedEntityView = checkNotNull(entityViewService.unassignEntityViewFromCustomer(tenantId, entityViewId));
notificationEntityService.notifyAssignOrUnassignEntityToCustomer(tenantId, entityViewId, customer.getId(), savedEntityView,
actionType, EdgeEventActionType.UNASSIGNED_FROM_CUSTOMER, user, true, customer.getId().toString(), customer.getName());
return savedEntityView;
} catch (Exception e) {
notificationEntityService.notifyEntity(tenantId, emptyId(EntityType.ENTITY_VIEW), null, null,
actionType, user, e, entityViewId.toString());
throw handleException(e);
}
}
private ListenableFuture<List<Void>> copyAttributesFromEntityToEntityView(EntityView entityView, String scope, Collection<String> keys, SecurityUser user) throws ThingsboardException {
EntityViewId entityId = entityView.getId();
if (keys != null && !keys.isEmpty()) {
ListenableFuture<List<AttributeKvEntry>> getAttrFuture = attributesService.find(entityView.getTenantId(), entityView.getEntityId(), scope, keys);
return Futures.transform(getAttrFuture, attributeKvEntries -> {
List<AttributeKvEntry> attributes;
if (attributeKvEntries != null && !attributeKvEntries.isEmpty()) {
attributes =
attributeKvEntries.stream()
.filter(attributeKvEntry -> {
long startTime = entityView.getStartTimeMs();
long endTime = entityView.getEndTimeMs();
long lastUpdateTs = attributeKvEntry.getLastUpdateTs();
return startTime == 0 && endTime == 0 ||
(endTime == 0 && startTime < lastUpdateTs) ||
(startTime == 0 && endTime > lastUpdateTs)
? true : startTime < lastUpdateTs && endTime > lastUpdateTs;
}).collect(Collectors.toList());
tsSubService.saveAndNotify(entityView.getTenantId(), entityId, scope, attributes, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void tmp) {
try {
logAttributesUpdated(entityView.getTenantId(), user, entityId, scope, attributes, null);
} catch (ThingsboardException e) {
log.error("Failed to log attribute updates", e);
}
}
@Override
public void onFailure(Throwable t) {
try {
logAttributesUpdated(entityView.getTenantId(), user, entityId, scope, attributes, t);
} catch (ThingsboardException e) {
log.error("Failed to log attribute updates", e);
}
}
});
}
return null;
}, MoreExecutors.directExecutor());
} else {
return Futures.immediateFuture(null);
}
}
private ListenableFuture<List<Void>> copyLatestFromEntityToEntityView(EntityView entityView, SecurityUser user) {
EntityViewId entityId = entityView.getId();
List<String> keys = entityView.getKeys() != null && entityView.getKeys().getTimeseries() != null ?
entityView.getKeys().getTimeseries() : Collections.emptyList();
long startTs = entityView.getStartTimeMs();
long endTs = entityView.getEndTimeMs() == 0 ? Long.MAX_VALUE : entityView.getEndTimeMs();
ListenableFuture<List<String>> keysFuture;
if (keys.isEmpty()) {
keysFuture = Futures.transform(tsService.findAllLatest(user.getTenantId(),
entityView.getEntityId()), latest -> latest.stream().map(TsKvEntry::getKey).collect(Collectors.toList()), MoreExecutors.directExecutor());
} else {
keysFuture = Futures.immediateFuture(keys);
}
ListenableFuture<List<TsKvEntry>> latestFuture = Futures.transformAsync(keysFuture, fetchKeys -> {
List<ReadTsKvQuery> queries = fetchKeys.stream().filter(key -> !isBlank(key)).map(key -> new BaseReadTsKvQuery(key, startTs, endTs, 1, "DESC")).collect(Collectors.toList());
if (!queries.isEmpty()) {
return tsService.findAll(user.getTenantId(), entityView.getEntityId(), queries);
} else {
return Futures.immediateFuture(null);
}
}, MoreExecutors.directExecutor());
return Futures.transform(latestFuture, latestValues -> {
if (latestValues != null && !latestValues.isEmpty()) {
tsSubService.saveLatestAndNotify(entityView.getTenantId(), entityId, latestValues, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void tmp) {
}
@Override
public void onFailure(Throwable t) {
}
});
}
return null;
}, MoreExecutors.directExecutor());
}
private ListenableFuture<Void> deleteAttributesFromEntityView(EntityView entityView, String scope, List<String> keys, SecurityUser user) {
EntityViewId entityId = entityView.getId();
SettableFuture<Void> resultFuture = SettableFuture.create();
if (keys != null && !keys.isEmpty()) {
tsSubService.deleteAndNotify(entityView.getTenantId(), entityId, scope, keys, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void tmp) {
try {
logAttributesDeleted(entityView.getTenantId(), user, entityId, scope, keys, null);
} catch (ThingsboardException e) {
log.error("Failed to log attribute delete", e);
}
resultFuture.set(tmp);
}
@Override
public void onFailure(Throwable t) {
try {
logAttributesDeleted(entityView.getTenantId(), user, entityId, scope, keys, t);
} catch (ThingsboardException e) {
log.error("Failed to log attribute delete", e);
}
resultFuture.setException(t);
}
});
} else {
resultFuture.set(null);
}
return resultFuture;
}
private ListenableFuture<Void> deleteLatestFromEntityView(EntityView entityView, List<String> keys, SecurityUser user) {
EntityViewId entityId = entityView.getId();
SettableFuture<Void> resultFuture = SettableFuture.create();
if (keys != null && !keys.isEmpty()) {
tsSubService.deleteLatest(entityView.getTenantId(), entityId, keys, new FutureCallback<Void>() {
@Override
public void onSuccess(@Nullable Void tmp) {
try {
logTimeseriesDeleted(entityView.getTenantId(), user, entityId, keys, null);
} catch (ThingsboardException e) {
log.error("Failed to log timeseries delete", e);
}
resultFuture.set(tmp);
}
@Override
public void onFailure(Throwable t) {
try {
logTimeseriesDeleted(entityView.getTenantId(),user, entityId, keys, t);
} catch (ThingsboardException e) {
log.error("Failed to log timeseries delete", e);
}
resultFuture.setException(t);
}
});
} else {
tsSubService.deleteAllLatest(entityView.getTenantId(), entityId, new FutureCallback<Collection<String>>() {
@Override
public void onSuccess(@Nullable Collection<String> keys) {
try {
logTimeseriesDeleted(entityView.getTenantId(), user, entityId, new ArrayList<>(keys), null);
} catch (ThingsboardException e) {
log.error("Failed to log timeseries delete", e);
}
resultFuture.set(null);
}
@Override
public void onFailure(Throwable t) {
try {
logTimeseriesDeleted(entityView.getTenantId(), user, entityId, Collections.emptyList(), t);
} catch (ThingsboardException e) {
log.error("Failed to log timeseries delete", e);
}
resultFuture.setException(t);
}
});
}
return resultFuture;
}
private void logAttributesUpdated(TenantId tenantId, SecurityUser user, EntityId entityId, String scope, List<AttributeKvEntry> attributes, Throwable e) throws ThingsboardException {
notificationEntityService.notifyEntity(tenantId, entityId, null, null, ActionType.ATTRIBUTES_UPDATED, user, toException(e), scope, attributes);
}
private void logAttributesDeleted(TenantId tenantId, SecurityUser user, EntityId entityId, String scope, List<String> keys, Throwable e) throws ThingsboardException {
notificationEntityService.notifyEntity(tenantId, entityId, null, null, ActionType.ATTRIBUTES_DELETED, user, toException(e), scope, keys);
}
private void logTimeseriesDeleted(TenantId tenantId, SecurityUser user, EntityId entityId, List<String> keys, Throwable e) throws ThingsboardException {
notificationEntityService.notifyEntity(tenantId, entityId, null, null, ActionType.TIMESERIES_DELETED, user, toException(e), keys);
}
public static Exception toException(Throwable error) {
return error != null ? (Exception.class.isInstance(error) ? (Exception) error : new Exception(error)) : null;
}
}

47
application/src/main/java/org/thingsboard/server/service/entitiy/entityView/TbEntityViewService.java

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2022 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.service.entitiy.entityView;
import org.thingsboard.server.common.data.Customer;
import org.thingsboard.server.common.data.EntityView;
import org.thingsboard.server.common.data.edge.Edge;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityViewId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TbEntityViewService {
EntityView save(EntityView entityView, EntityView existingEntityView, SecurityUser user) throws ThingsboardException;
void delete (EntityView entity, SecurityUser user) throws ThingsboardException;
EntityView assignEntityViewToCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer,
SecurityUser user) throws ThingsboardException;
EntityView assignEntityViewToPublicCustomer(TenantId tenantId, CustomerId customerId, Customer publicCustomer,
EntityViewId entityViewId, SecurityUser user) throws ThingsboardException;
EntityView assignEntityViewToEdge(TenantId tenantId, CustomerId customerId, EntityViewId entityViewId, Edge edge,
SecurityUser user) throws ThingsboardException;
EntityView unassignEntityViewFromEdge(TenantId tenantId, CustomerId customerId, EntityView entityView,
Edge edge, SecurityUser user) throws ThingsboardException;
EntityView unassignEntityViewFromCustomer(TenantId tenantId, EntityViewId entityViewId, Customer customer,
SecurityUser user) throws ThingsboardException;
}

2
application/src/main/java/org/thingsboard/server/service/install/SqlDatabaseUpgradeService.java

@ -638,8 +638,6 @@ public class SqlDatabaseUpgradeService implements DatabaseEntitiesUpgradeService
});
profilePageLink = profilePageLink.nextPageLink();
} while (profilePageData.hasNext());
log.info("Updating schema settings...");
conn.createStatement().execute("UPDATE tb_schema_settings SET schema_version = 3004000;");
log.info("Schema updated.");

21
application/src/main/java/org/thingsboard/server/service/mail/DefaultMailService.java

@ -40,7 +40,6 @@ import org.thingsboard.server.common.data.ApiUsageStateValue;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.exception.IncorrectParameterException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
@ -48,6 +47,7 @@ import org.thingsboard.server.queue.usagestats.TbApiUsageClient;
import org.thingsboard.server.service.apiusage.TbApiUsageStateService;
import javax.annotation.PostConstruct;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.ByteArrayInputStream;
import java.util.HashMap;
@ -101,7 +101,7 @@ public class DefaultMailService implements MailService {
mailSender = createMailSender(jsonConfig);
mailFrom = jsonConfig.get("mailFrom").asText();
} else {
throw new IncorrectParameterException("Failed to date mail configuration. Settings not found!");
throw new IncorrectParameterException("Failed to update mail configuration. Settings not found!");
}
}
@ -311,6 +311,18 @@ public class DefaultMailService implements MailService {
sendMail(mailSender, mailFrom, email, subject, message);
}
@Override
public void sendTwoFaVerificationEmail(String email, String verificationCode, int expirationTimeSeconds) throws ThingsboardException {
String subject = messages.getMessage("2fa.verification.code.subject", null, Locale.US);
String message = mergeTemplateIntoString("2fa.verification.code.ftl", Map.of(
TARGET_EMAIL, email,
"code", verificationCode,
"expirationTimeSeconds", expirationTimeSeconds
));
sendMail(mailSender, mailFrom, email, subject, message);
}
@Override
public void sendApiFeatureStateEmail(ApiFeature apiFeature, ApiUsageStateValue stateValue, String email, ApiUsageStateMailMessage msg) throws ThingsboardException {
String subject = messages.getMessage("api.usage.state", null, Locale.US);
@ -338,6 +350,11 @@ public class DefaultMailService implements MailService {
sendMail(mailSender, mailFrom, email, subject, message);
}
@Override
public void testConnection(TenantId tenantId) throws Exception {
mailSender.testConnection();
}
private String toEnabledValueLabel(ApiFeature apiFeature) {
switch (apiFeature) {
case DB:

24
application/src/main/java/org/thingsboard/server/service/security/auth/MfaAuthenticationToken.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2022 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.service.security.auth;
import org.thingsboard.server.service.security.model.SecurityUser;
public class MfaAuthenticationToken extends AbstractJwtAuthenticationToken {
public MfaAuthenticationToken(SecurityUser securityUser) {
super(securityUser);
}
}

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

@ -0,0 +1,190 @@
/**
* Copyright © 2016-2022 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.service.security.auth.mfa;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException;
import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
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.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.common.msg.tools.TbRateLimits;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
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;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import java.util.Collection;
import java.util.EnumMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Service
@RequiredArgsConstructor
@TbCoreComponent
public class DefaultTwoFactorAuthService implements TwoFactorAuthService {
private final TwoFaConfigManager configManager;
private final SystemSecurityService systemSecurityService;
private final UserService userService;
private final Map<TwoFaProviderType, TwoFaProvider<TwoFaProviderConfig, TwoFaAccountConfig>> providers = new EnumMap<>(TwoFaProviderType.class);
private static final ThingsboardException ACCOUNT_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA is not configured for account", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
private static final ThingsboardException PROVIDER_NOT_CONFIGURED_ERROR = new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
private static final ThingsboardException PROVIDER_NOT_AVAILABLE_ERROR = new ThingsboardException("2FA provider is not available", ThingsboardErrorCode.GENERAL);
private final ConcurrentMap<UserId, ConcurrentMap<TwoFaProviderType, TbRateLimits>> verificationCodeSendingRateLimits = new ConcurrentHashMap<>();
private final ConcurrentMap<UserId, ConcurrentMap<TwoFaProviderType, TbRateLimits>> verificationCodeCheckingRateLimits = new ConcurrentHashMap<>();
@Override
public boolean isTwoFaEnabled(TenantId tenantId, UserId userId) {
return configManager.getAccountTwoFaSettings(tenantId, userId)
.map(settings -> !settings.getConfigs().isEmpty())
.orElse(false);
}
@Override
public void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException {
getTwoFaProvider(providerType).check(tenantId);
}
@Override
public void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception {
TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType)
.orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR);
prepareVerificationCode(user, accountConfig, checkLimits);
}
@Override
public void prepareVerificationCode(SecurityUser user, TwoFaAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException {
PlatformTwoFaSettings twoFaSettings = configManager.getPlatformTwoFaSettings(user.getTenantId(), true)
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
if (checkLimits) {
Integer minVerificationCodeSendPeriod = twoFaSettings.getMinVerificationCodeSendPeriod();
String rateLimit = null;
if (minVerificationCodeSendPeriod != null && minVerificationCodeSendPeriod > 4) {
rateLimit = "1:" + minVerificationCodeSendPeriod;
}
checkRateLimits(user.getId(), accountConfig.getProviderType(), rateLimit, verificationCodeSendingRateLimits);
}
TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
getTwoFaProvider(accountConfig.getProviderType()).prepareVerificationCode(user, providerConfig, accountConfig);
}
@Override
public boolean checkVerificationCode(SecurityUser user, TwoFaProviderType providerType, String verificationCode, boolean checkLimits) throws ThingsboardException {
TwoFaAccountConfig accountConfig = configManager.getTwoFaAccountConfig(user.getTenantId(), user.getId(), providerType)
.orElseThrow(() -> ACCOUNT_NOT_CONFIGURED_ERROR);
return checkVerificationCode(user, verificationCode, accountConfig, checkLimits);
}
@Override
public boolean checkVerificationCode(SecurityUser user, String verificationCode, TwoFaAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException {
if (!userService.findUserCredentialsByUserId(user.getTenantId(), user.getId()).isEnabled()) {
throw new ThingsboardException("User is disabled", ThingsboardErrorCode.AUTHENTICATION);
}
PlatformTwoFaSettings twoFaSettings = configManager.getPlatformTwoFaSettings(user.getTenantId(), true)
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
if (checkLimits) {
checkRateLimits(user.getId(), accountConfig.getProviderType(), twoFaSettings.getVerificationCodeCheckRateLimit(), verificationCodeCheckingRateLimits);
}
TwoFaProviderConfig providerConfig = twoFaSettings.getProviderConfig(accountConfig.getProviderType())
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
boolean verificationSuccess = false;
if (StringUtils.isNotBlank(verificationCode)) {
if (StringUtils.isNumeric(verificationCode) || accountConfig.getProviderType() == TwoFaProviderType.BACKUP_CODE) {
verificationSuccess = getTwoFaProvider(accountConfig.getProviderType()).checkVerificationCode(user, verificationCode, providerConfig, accountConfig);
}
}
if (checkLimits) {
try {
systemSecurityService.validateTwoFaVerification(user, verificationSuccess, twoFaSettings);
} catch (LockedException e) {
verificationCodeCheckingRateLimits.remove(user.getId());
verificationCodeSendingRateLimits.remove(user.getId());
throw new ThingsboardException(e.getMessage(), ThingsboardErrorCode.AUTHENTICATION);
}
if (verificationSuccess) {
verificationCodeCheckingRateLimits.remove(user.getId());
verificationCodeSendingRateLimits.remove(user.getId());
}
}
return verificationSuccess;
}
private void checkRateLimits(UserId userId, TwoFaProviderType providerType, String rateLimitConfig,
ConcurrentMap<UserId, ConcurrentMap<TwoFaProviderType, TbRateLimits>> rateLimits) throws ThingsboardException {
if (StringUtils.isNotEmpty(rateLimitConfig)) {
ConcurrentMap<TwoFaProviderType, TbRateLimits> providersRateLimits = rateLimits.computeIfAbsent(userId, i -> new ConcurrentHashMap<>());
TbRateLimits rateLimit = providersRateLimits.get(providerType);
if (rateLimit == null || !rateLimit.getConfiguration().equals(rateLimitConfig)) {
rateLimit = new TbRateLimits(rateLimitConfig, true);
providersRateLimits.put(providerType, rateLimit);
}
if (!rateLimit.tryConsume()) {
throw new ThingsboardException("Too many requests", ThingsboardErrorCode.TOO_MANY_REQUESTS);
}
} else {
rateLimits.remove(userId);
}
}
@Override
public TwoFaAccountConfig generateNewAccountConfig(User user, TwoFaProviderType providerType) throws ThingsboardException {
TwoFaProviderConfig providerConfig = getTwoFaProviderConfig(user.getTenantId(), providerType);
return getTwoFaProvider(providerType).generateNewAccountConfig(user, providerConfig);
}
private TwoFaProviderConfig getTwoFaProviderConfig(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException {
return configManager.getPlatformTwoFaSettings(tenantId, true)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType))
.orElseThrow(() -> PROVIDER_NOT_CONFIGURED_ERROR);
}
private TwoFaProvider<TwoFaProviderConfig, TwoFaAccountConfig> getTwoFaProvider(TwoFaProviderType providerType) throws ThingsboardException {
return Optional.ofNullable(providers.get(providerType))
.orElseThrow(() -> PROVIDER_NOT_AVAILABLE_ERROR);
}
@Autowired
private void setProviders(Collection<TwoFaProvider> providers) {
providers.forEach(provider -> {
this.providers.put(provider.getType(), provider);
});
}
}

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

@ -0,0 +1,45 @@
/**
* Copyright © 2016-2022 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.service.security.auth.mfa;
import org.thingsboard.server.common.data.User;
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.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TwoFactorAuthService {
boolean isTwoFaEnabled(TenantId tenantId, UserId userId);
void checkProvider(TenantId tenantId, TwoFaProviderType providerType) throws ThingsboardException;
void prepareVerificationCode(SecurityUser user, TwoFaProviderType providerType, boolean checkLimits) throws Exception;
void prepareVerificationCode(SecurityUser user, TwoFaAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException;
boolean checkVerificationCode(SecurityUser user, TwoFaProviderType providerType, String verificationCode, boolean checkLimits) throws ThingsboardException;
boolean checkVerificationCode(SecurityUser user, String verificationCode, TwoFaAccountConfig accountConfig, boolean checkLimits) throws ThingsboardException;
TwoFaAccountConfig generateNewAccountConfig(User user, TwoFaProviderType providerType) throws ThingsboardException;
}

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

@ -0,0 +1,168 @@
/**
* Copyright © 2016-2022 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.service.security.auth.mfa.config;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.AdminSettings;
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.security.UserAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.dao.service.ConstraintValidator;
import org.thingsboard.server.dao.settings.AdminSettingsDao;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserAuthSettingsDao;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class DefaultTwoFaConfigManager implements TwoFaConfigManager {
private final UserAuthSettingsDao userAuthSettingsDao;
private final AdminSettingsService adminSettingsService;
private final AdminSettingsDao adminSettingsDao;
@Autowired @Lazy
private TwoFactorAuthService twoFactorAuthService;
protected static final String TWO_FACTOR_AUTH_SETTINGS_KEY = "twoFaSettings";
@Override
public Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId) {
return Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
.flatMap(userAuthSettings -> Optional.ofNullable(userAuthSettings.getTwoFaSettings()))
.map(twoFaSettings -> {
twoFaSettings.getConfigs().keySet().removeIf(providerType -> {
return getTwoFaProviderConfig(tenantId, providerType).isEmpty();
});
return twoFaSettings;
});
}
protected AccountTwoFaSettings saveAccountTwoFaSettings(TenantId tenantId, UserId userId, AccountTwoFaSettings settings) {
UserAuthSettings userAuthSettings = Optional.ofNullable(userAuthSettingsDao.findByUserId(userId))
.orElseGet(() -> {
UserAuthSettings newUserAuthSettings = new UserAuthSettings();
newUserAuthSettings.setUserId(userId);
return newUserAuthSettings;
});
userAuthSettings.setTwoFaSettings(settings);
settings.getConfigs().values().forEach(accountConfig -> accountConfig.setSerializeHiddenFields(true));
userAuthSettingsDao.save(tenantId, userAuthSettings);
settings.getConfigs().values().forEach(accountConfig -> accountConfig.setSerializeHiddenFields(false));
return settings;
}
@Override
public Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) {
return getAccountTwoFaSettings(tenantId, userId)
.map(AccountTwoFaSettings::getConfigs)
.flatMap(configs -> Optional.ofNullable(configs.get(providerType)));
}
@Override
public AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig) {
getTwoFaProviderConfig(tenantId, accountConfig.getProviderType())
.orElseThrow(() -> new IllegalArgumentException("2FA provider is not configured"));
AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId).orElseGet(() -> {
AccountTwoFaSettings newSettings = new AccountTwoFaSettings();
newSettings.setConfigs(new LinkedHashMap<>());
return newSettings;
});
Map<TwoFaProviderType, TwoFaAccountConfig> configs = settings.getConfigs();
if (configs.isEmpty() && accountConfig.getProviderType() == TwoFaProviderType.BACKUP_CODE) {
throw new IllegalArgumentException("To use 2FA backup codes you first need to configure at least one provider");
}
if (accountConfig.isUseByDefault()) {
configs.values().forEach(config -> config.setUseByDefault(false));
}
configs.put(accountConfig.getProviderType(), accountConfig);
if (configs.values().stream().noneMatch(TwoFaAccountConfig::isUseByDefault)) {
configs.values().stream().findFirst().ifPresent(config -> config.setUseByDefault(true));
}
return saveAccountTwoFaSettings(tenantId, userId, settings);
}
@Override
public AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType) {
AccountTwoFaSettings settings = getAccountTwoFaSettings(tenantId, userId)
.orElseThrow(() -> new IllegalArgumentException("2FA not configured"));
settings.getConfigs().remove(providerType);
if (settings.getConfigs().size() == 1) {
settings.getConfigs().remove(TwoFaProviderType.BACKUP_CODE);
}
if (!settings.getConfigs().isEmpty() && settings.getConfigs().values().stream()
.noneMatch(TwoFaAccountConfig::isUseByDefault)) {
settings.getConfigs().values().stream()
.min(Comparator.comparing(TwoFaAccountConfig::getProviderType))
.ifPresent(config -> config.setUseByDefault(true));
}
return saveAccountTwoFaSettings(tenantId, userId, settings);
}
private Optional<TwoFaProviderConfig> getTwoFaProviderConfig(TenantId tenantId, TwoFaProviderType providerType) {
return getPlatformTwoFaSettings(tenantId, true)
.flatMap(twoFaSettings -> twoFaSettings.getProviderConfig(providerType));
}
@Override
public Optional<PlatformTwoFaSettings> getPlatformTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault) {
return Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(TenantId.SYS_TENANT_ID, TWO_FACTOR_AUTH_SETTINGS_KEY))
.map(adminSettings -> JacksonUtil.treeToValue(adminSettings.getJsonValue(), PlatformTwoFaSettings.class));
}
@Override
public PlatformTwoFaSettings savePlatformTwoFaSettings(TenantId tenantId, PlatformTwoFaSettings twoFactorAuthSettings) throws ThingsboardException {
ConstraintValidator.validateFields(twoFactorAuthSettings);
for (TwoFaProviderConfig providerConfig : twoFactorAuthSettings.getProviders()) {
twoFactorAuthService.checkProvider(tenantId, providerConfig.getProviderType());
}
AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.orElseGet(() -> {
AdminSettings newSettings = new AdminSettings();
newSettings.setKey(TWO_FACTOR_AUTH_SETTINGS_KEY);
return newSettings;
});
settings.setJsonValue(JacksonUtil.valueToTree(twoFactorAuthSettings));
adminSettingsService.saveAdminSettings(tenantId, settings);
return twoFactorAuthSettings;
}
@Override
public void deletePlatformTwoFaSettings(TenantId tenantId) {
Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY))
.ifPresent(adminSettings -> adminSettingsDao.removeById(tenantId, adminSettings.getId().getId()));
}
}

46
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/TwoFaConfigManager.java

@ -0,0 +1,46 @@
/**
* Copyright © 2016-2022 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.service.security.auth.mfa.config;
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.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import java.util.Optional;
public interface TwoFaConfigManager {
Optional<AccountTwoFaSettings> getAccountTwoFaSettings(TenantId tenantId, UserId userId);
Optional<TwoFaAccountConfig> getTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType);
AccountTwoFaSettings saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaAccountConfig accountConfig);
AccountTwoFaSettings deleteTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFaProviderType providerType);
Optional<PlatformTwoFaSettings> getPlatformTwoFaSettings(TenantId tenantId, boolean sysadminSettingsAsDefault);
PlatformTwoFaSettings savePlatformTwoFaSettings(TenantId tenantId, PlatformTwoFaSettings twoFactorAuthSettings) throws ThingsboardException;
void deletePlatformTwoFaSettings(TenantId tenantId);
}

39
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFaProvider.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2022 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.service.security.auth.mfa.provider;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.service.security.model.SecurityUser;
public interface TwoFaProvider<C extends TwoFaProviderConfig, A extends TwoFaAccountConfig> {
A generateNewAccountConfig(User user, C providerConfig);
default void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) throws ThingsboardException {}
boolean checkVerificationCode(SecurityUser user, String code, C providerConfig, A accountConfig);
default void check(TenantId tenantId) throws ThingsboardException {};
TwoFaProviderType getType();
}

73
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/BackupCodeTwoFaProvider.java

@ -0,0 +1,73 @@
/**
* Copyright © 2016-2022 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.service.security.auth.mfa.provider.impl;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.CollectionsUtil;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.model.mfa.account.BackupCodeTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.BackupCodeTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Service
@TbCoreComponent
public class BackupCodeTwoFaProvider implements TwoFaProvider<BackupCodeTwoFaProviderConfig, BackupCodeTwoFaAccountConfig> {
@Autowired @Lazy
private TwoFaConfigManager twoFaConfigManager;
@Override
public BackupCodeTwoFaAccountConfig generateNewAccountConfig(User user, BackupCodeTwoFaProviderConfig providerConfig) {
BackupCodeTwoFaAccountConfig config = new BackupCodeTwoFaAccountConfig();
config.setCodes(generateCodes(providerConfig.getCodesQuantity(), 8));
config.setSerializeHiddenFields(true);
return config;
}
private static Set<String> generateCodes(int count, int length) {
return Stream.generate(() -> RandomStringUtils.random(length, "0123456789abcdef"))
.distinct().limit(count)
.collect(Collectors.toSet());
}
@Override
public boolean checkVerificationCode(SecurityUser user, String code, BackupCodeTwoFaProviderConfig providerConfig, BackupCodeTwoFaAccountConfig accountConfig) {
if (CollectionsUtil.contains(accountConfig.getCodes(), code)) {
accountConfig.getCodes().remove(code);
twoFaConfigManager.saveTwoFaAccountConfig(user.getTenantId(), user.getId(), accountConfig);
return true;
} else {
return false;
}
}
@Override
public TwoFaProviderType getType() {
return TwoFaProviderType.BACKUP_CODE;
}
}

68
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/EmailTwoFaProvider.java

@ -0,0 +1,68 @@
/**
* Copyright © 2016-2022 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.service.security.auth.mfa.provider.impl;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.mfa.account.EmailTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.EmailTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
@Service
@TbCoreComponent
public class EmailTwoFaProvider extends OtpBasedTwoFaProvider<EmailTwoFaProviderConfig, EmailTwoFaAccountConfig> {
private final MailService mailService;
protected EmailTwoFaProvider(CacheManager cacheManager, MailService mailService) {
super(cacheManager);
this.mailService = mailService;
}
@Override
public EmailTwoFaAccountConfig generateNewAccountConfig(User user, EmailTwoFaProviderConfig providerConfig) {
EmailTwoFaAccountConfig config = new EmailTwoFaAccountConfig();
config.setEmail(user.getEmail());
return config;
}
@Override
public void check(TenantId tenantId) throws ThingsboardException {
try {
mailService.testConnection(tenantId);
} catch (Exception e) {
throw new ThingsboardException("Mail service is not set up", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
}
@Override
protected void sendVerificationCode(SecurityUser user, String verificationCode, EmailTwoFaProviderConfig providerConfig, EmailTwoFaAccountConfig accountConfig) throws ThingsboardException {
mailService.sendTwoFaVerificationEmail(accountConfig.getEmail(), verificationCode, providerConfig.getVerificationCodeLifetime());
}
@Override
public TwoFaProviderType getType() {
return TwoFaProviderType.EMAIL;
}
}

76
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/OtpBasedTwoFaProvider.java

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2022 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.service.security.auth.mfa.provider.impl;
import lombok.Data;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.security.model.mfa.account.OtpBasedTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.OtpBasedTwoFaProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.concurrent.TimeUnit;
public abstract class OtpBasedTwoFaProvider<C extends OtpBasedTwoFaProviderConfig, A extends OtpBasedTwoFaAccountConfig> implements TwoFaProvider<C, A> {
private final Cache verificationCodesCache;
protected OtpBasedTwoFaProvider(CacheManager cacheManager) {
this.verificationCodesCache = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE);
}
@Override
public final void prepareVerificationCode(SecurityUser user, C providerConfig, A accountConfig) throws ThingsboardException {
String verificationCode = RandomStringUtils.randomNumeric(6);
sendVerificationCode(user, verificationCode, providerConfig, accountConfig);
verificationCodesCache.put(user.getId(), new Otp(System.currentTimeMillis(), verificationCode, accountConfig));
}
protected abstract void sendVerificationCode(SecurityUser user, String verificationCode, C providerConfig, A accountConfig) throws ThingsboardException;
@Override
public final boolean checkVerificationCode(SecurityUser user, String code, C providerConfig, A accountConfig) {
Otp correctVerificationCode = verificationCodesCache.get(user.getId(), Otp.class);
if (correctVerificationCode != null) {
if (System.currentTimeMillis() - correctVerificationCode.getTimestamp()
> TimeUnit.SECONDS.toMillis(providerConfig.getVerificationCodeLifetime())) {
verificationCodesCache.evict(user.getId());
return false;
}
if (code.equals(correctVerificationCode.getValue())
&& accountConfig.equals(correctVerificationCode.getAccountConfig())) {
verificationCodesCache.evict(user.getId());
return true;
}
}
return false;
}
@Data
public static class Otp {
private final long timestamp;
private final String value;
private final OtpBasedTwoFaAccountConfig accountConfig;
}
}

76
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/SmsTwoFaProvider.java

@ -0,0 +1,76 @@
/**
* Copyright © 2016-2022 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.service.security.auth.mfa.provider.impl;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.rule.engine.api.util.TbNodeUtils;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import java.util.Map;
@Service
@TbCoreComponent
public class SmsTwoFaProvider extends OtpBasedTwoFaProvider<SmsTwoFaProviderConfig, SmsTwoFaAccountConfig> {
private final SmsService smsService;
public SmsTwoFaProvider(CacheManager cacheManager, SmsService smsService) {
super(cacheManager);
this.smsService = smsService;
}
@Override
public SmsTwoFaAccountConfig generateNewAccountConfig(User user, SmsTwoFaProviderConfig providerConfig) {
return new SmsTwoFaAccountConfig();
}
@Override
protected void sendVerificationCode(SecurityUser user, String verificationCode, SmsTwoFaProviderConfig providerConfig, SmsTwoFaAccountConfig accountConfig) throws ThingsboardException {
Map<String, String> messageData = Map.of(
"code", verificationCode,
"userEmail", user.getEmail()
);
String message = TbNodeUtils.processTemplate(providerConfig.getSmsVerificationMessageTemplate(), messageData);
String phoneNumber = accountConfig.getPhoneNumber();
smsService.sendSms(user.getTenantId(), user.getCustomerId(), new String[]{phoneNumber}, message);
}
@Override
public void check(TenantId tenantId) throws ThingsboardException {
if (!smsService.isConfigured(tenantId)) {
throw new ThingsboardException("SMS service in not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS);
}
}
@Override
public TwoFaProviderType getType() {
return TwoFaProviderType.SMS;
}
}

74
application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/impl/TotpTwoFaProvider.java

@ -0,0 +1,74 @@
/**
* Copyright © 2016-2022 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.service.security.auth.mfa.provider.impl;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.apache.commons.lang3.RandomUtils;
import org.apache.http.client.utils.URIBuilder;
import org.jboss.aerogear.security.otp.Totp;
import org.jboss.aerogear.security.otp.api.Base32;
import org.springframework.stereotype.Service;
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFaProviderConfig;
import org.thingsboard.server.service.security.auth.mfa.provider.TwoFaProvider;
import org.thingsboard.server.service.security.model.SecurityUser;
@Service
@RequiredArgsConstructor
@TbCoreComponent
public class TotpTwoFaProvider implements TwoFaProvider<TotpTwoFaProviderConfig, TotpTwoFaAccountConfig> {
@Override
public final TotpTwoFaAccountConfig generateNewAccountConfig(User user, TotpTwoFaProviderConfig providerConfig) {
TotpTwoFaAccountConfig config = new TotpTwoFaAccountConfig();
String secretKey = generateSecretKey();
config.setAuthUrl(getTotpAuthUrl(user, secretKey, providerConfig));
return config;
}
@Override
public final boolean checkVerificationCode(SecurityUser user, String code, TotpTwoFaProviderConfig providerConfig, TotpTwoFaAccountConfig accountConfig) {
String secretKey = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build().getQueryParams().getFirst("secret");
return new Totp(secretKey).verify(code);
}
@SneakyThrows
private String getTotpAuthUrl(User user, String secretKey, TotpTwoFaProviderConfig providerConfig) {
URIBuilder uri = new URIBuilder()
.setScheme("otpauth")
.setHost("totp")
.setParameter("issuer", providerConfig.getIssuerName())
.setPath("/" + providerConfig.getIssuerName() + ":" + user.getEmail())
.setParameter("secret", secretKey);
return uri.build().toASCIIString();
}
private String generateSecretKey() {
return Base32.encode(RandomUtils.nextBytes(20));
}
@Override
public TwoFaProviderType getType() {
return TwoFaProviderType.TOTP;
}
}

2
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/AppleOAuth2ClientMapper.java

@ -26,6 +26,7 @@ import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
@ -34,6 +35,7 @@ import java.util.Map;
@Service(value = "appleOAuth2ClientMapper")
@Slf4j
@TbCoreComponent
public class AppleOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
private static final String USER = "user";

2
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/BasicOAuth2ClientMapper.java

@ -21,6 +21,7 @@ import org.springframework.stereotype.Service;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
@ -28,6 +29,7 @@ import java.util.Map;
@Service(value = "basicOAuth2ClientMapper")
@Slf4j
@TbCoreComponent
public class BasicOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
@Override

2
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/CustomOAuth2ClientMapper.java

@ -28,6 +28,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2CustomMapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.annotation.PostConstruct;
@ -35,6 +36,7 @@ import javax.servlet.http.HttpServletRequest;
@Service(value = "customOAuth2ClientMapper")
@Slf4j
@TbCoreComponent
public class CustomOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
private static final String PROVIDER_ACCESS_TOKEN = "provider-access-token";

2
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/GithubOAuth2ClientMapper.java

@ -27,6 +27,7 @@ import org.thingsboard.server.common.data.oauth2.OAuth2MapperConfig;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.dao.oauth2.OAuth2Configuration;
import org.thingsboard.server.dao.oauth2.OAuth2User;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
@ -36,6 +37,7 @@ import java.util.Optional;
@Service(value = "githubOAuth2ClientMapper")
@Slf4j
@TbCoreComponent
public class GithubOAuth2ClientMapper extends AbstractOAuth2ClientMapper implements OAuth2ClientMapper {
private static final String EMAIL_URL_KEY = "emailUrl";

2
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/OAuth2ClientMapperProvider.java

@ -20,9 +20,11 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.oauth2.MapperType;
import org.thingsboard.server.queue.util.TbCoreComponent;
@Component
@Slf4j
@TbCoreComponent
public class OAuth2ClientMapperProvider {
@Autowired

2
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationFailureHandler.java

@ -25,6 +25,7 @@ import org.thingsboard.server.common.data.StringUtils;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import org.thingsboard.server.utils.MiscUtils;
@ -35,6 +36,7 @@ import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
@TbCoreComponent
@Component(value = "oauth2AuthenticationFailureHandler")
public class Oauth2AuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

2
application/src/main/java/org/thingsboard/server/service/security/auth/oauth2/Oauth2AuthenticationSuccessHandler.java

@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.oauth2.OAuth2Registration;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.dao.oauth2.OAuth2Service;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -45,6 +46,7 @@ import java.util.UUID;
@Slf4j
@Component(value = "oauth2AuthenticationSuccessHandler")
@TbCoreComponent
public class Oauth2AuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenFactory tokenFactory;

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

@ -36,35 +36,37 @@ import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.customer.CustomerService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.system.SystemSecurityService;
import ua_parser.Client;
import java.util.UUID;
@Component
@Slf4j
@TbCoreComponent
public class RestAuthenticationProvider implements AuthenticationProvider {
private final SystemSecurityService systemSecurityService;
private final UserService userService;
private final CustomerService customerService;
private final AuditLogService auditLogService;
private final TwoFactorAuthService twoFactorAuthService;
@Autowired
public RestAuthenticationProvider(final UserService userService,
final CustomerService customerService,
final SystemSecurityService systemSecurityService,
final AuditLogService auditLogService) {
TwoFactorAuthService twoFactorAuthService) {
this.userService = userService;
this.customerService = customerService;
this.systemSecurityService = systemSecurityService;
this.auditLogService = auditLogService;
this.twoFactorAuthService = twoFactorAuthService;
}
@Override
@ -77,17 +79,25 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
}
UserPrincipal userPrincipal = (UserPrincipal) principal;
SecurityUser securityUser;
if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) {
String username = userPrincipal.getValue();
String password = (String) authentication.getCredentials();
return authenticateByUsernameAndPassword(authentication, userPrincipal, username, password);
securityUser = authenticateByUsernameAndPassword(authentication, userPrincipal, username, password);
if (twoFactorAuthService.isTwoFaEnabled(securityUser.getTenantId(), securityUser.getId())) {
return new MfaAuthenticationToken(securityUser);
} else {
systemSecurityService.logLoginAction(securityUser, authentication.getDetails(), ActionType.LOGIN, null);
}
} else {
String publicId = userPrincipal.getValue();
return authenticateByPublicId(userPrincipal, publicId);
securityUser = authenticateByPublicId(userPrincipal, publicId);
}
return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
}
private Authentication authenticateByUsernameAndPassword(Authentication authentication, UserPrincipal userPrincipal, String username, String password) {
private SecurityUser authenticateByUsernameAndPassword(Authentication authentication, UserPrincipal userPrincipal, String username, String password) {
User user = userService.findUserByEmail(TenantId.SYS_TENANT_ID, username);
if (user == null) {
throw new UsernameNotFoundException("User not found: " + username);
@ -103,23 +113,21 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
try {
systemSecurityService.validateUserCredentials(user.getTenantId(), userCredentials, username, password);
} catch (LockedException e) {
logLoginAction(user, authentication, ActionType.LOCKOUT, null);
systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOCKOUT, null);
throw e;
}
if (user.getAuthority() == null)
throw new InsufficientAuthenticationException("User has no authority assigned");
SecurityUser securityUser = new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
logLoginAction(user, authentication, ActionType.LOGIN, null);
return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
return new SecurityUser(user, userCredentials.isEnabled(), userPrincipal);
} catch (Exception e) {
logLoginAction(user, authentication, ActionType.LOGIN, e);
systemSecurityService.logLoginAction(user, authentication.getDetails(), ActionType.LOGIN, e);
throw e;
}
}
private Authentication authenticateByPublicId(UserPrincipal userPrincipal, String publicId) {
private SecurityUser authenticateByPublicId(UserPrincipal userPrincipal, String publicId) {
CustomerId customerId;
try {
customerId = new CustomerId(UUID.fromString(publicId));
@ -141,9 +149,7 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
user.setFirstName("Public");
user.setLastName("Public");
SecurityUser securityUser = new SecurityUser(user, true, userPrincipal);
return new UsernamePasswordAuthenticationToken(securityUser, null, securityUser.getAuthorities());
return new SecurityUser(user, true, userPrincipal);
}
@Override
@ -151,52 +157,4 @@ public class RestAuthenticationProvider implements AuthenticationProvider {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
private void logLoginAction(User user, Authentication authentication, ActionType actionType, Exception e) {
String clientAddress = "Unknown";
String browser = "Unknown";
String os = "Unknown";
String device = "Unknown";
if (authentication != null && authentication.getDetails() != null) {
if (authentication.getDetails() instanceof RestAuthenticationDetails) {
RestAuthenticationDetails details = (RestAuthenticationDetails)authentication.getDetails();
clientAddress = details.getClientAddress();
if (details.getUserAgent() != null) {
Client userAgent = details.getUserAgent();
if (userAgent.userAgent != null) {
browser = userAgent.userAgent.family;
if (userAgent.userAgent.major != null) {
browser += " " + userAgent.userAgent.major;
if (userAgent.userAgent.minor != null) {
browser += "." + userAgent.userAgent.minor;
if (userAgent.userAgent.patch != null) {
browser += "." + userAgent.userAgent.patch;
}
}
}
}
if (userAgent.os != null) {
os = userAgent.os.family;
if (userAgent.os.major != null) {
os += " " + userAgent.os.major;
if (userAgent.os.minor != null) {
os += "." + userAgent.os.minor;
if (userAgent.os.patch != null) {
os += "." + userAgent.os.patch;
if (userAgent.os.patchMinor != null) {
os += "." + userAgent.os.patchMinor;
}
}
}
}
}
if (userAgent.device != null) {
device = userAgent.device.family;
}
}
}
}
auditLogService.logEntityAction(
user.getTenantId(), user.getCustomerId(), user.getId(),
user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device);
}
}

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

@ -16,15 +16,18 @@
package org.thingsboard.server.service.security.auth.rest;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.WebAttributes;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.service.security.auth.MfaAuthenticationToken;
import org.thingsboard.server.service.security.auth.jwt.RefreshTokenRepository;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
@ -33,37 +36,39 @@ import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
@Component(value = "defaultAuthenticationSuccessHandler")
@RequiredArgsConstructor
public class RestAwareAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
private final ObjectMapper mapper;
private final JwtTokenFactory tokenFactory;
private final TwoFaConfigManager twoFaConfigManager;
private final RefreshTokenRepository refreshTokenRepository;
@Autowired
public RestAwareAuthenticationSuccessHandler(final ObjectMapper mapper, final JwtTokenFactory tokenFactory, final RefreshTokenRepository refreshTokenRepository) {
this.mapper = mapper;
this.tokenFactory = tokenFactory;
this.refreshTokenRepository = refreshTokenRepository;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
SecurityUser securityUser = (SecurityUser) authentication.getPrincipal();
JwtTokenPair tokenPair = new JwtTokenPair();
JwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
JwtToken refreshToken = refreshTokenRepository.requestRefreshToken(securityUser);
Map<String, String> tokenMap = new HashMap<String, String>();
tokenMap.put("token", accessToken.getToken());
tokenMap.put("refreshToken", refreshToken.getToken());
if (authentication instanceof MfaAuthenticationToken) {
int preVerificationTokenLifetime = twoFaConfigManager.getPlatformTwoFaSettings(securityUser.getTenantId(), true)
.flatMap(settings -> Optional.ofNullable(settings.getTotalAllowedTimeForVerification())
.filter(time -> time > 0))
.orElse((int) TimeUnit.MINUTES.toSeconds(30));
tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser, preVerificationTokenLifetime).getToken());
tokenPair.setRefreshToken(null);
tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN);
} else {
tokenPair.setToken(tokenFactory.createAccessJwtToken(securityUser).getToken());
tokenPair.setRefreshToken(refreshTokenRepository.requestRefreshToken(securityUser).getToken());
}
response.setStatus(HttpStatus.OK.value());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
mapper.writeValue(response.getWriter(), tokenMap);
mapper.writeValue(response.getWriter(), tokenPair);
clearAuthenticationAttributes(request);
}

12
application/src/main/java/org/thingsboard/server/service/security/model/JwtTokenPair.java

@ -19,14 +19,24 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.thingsboard.server.common.data.security.Authority;
@ApiModel(value = "JWT Token Pair")
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JwtTokenPair {
@ApiModelProperty(position = 1, value = "The JWT Access Token. Used to perform API calls.", example = "AAB254FF67D..")
private String token;
@ApiModelProperty(position = 1, value = "The JWT Refresh Token. Used to get new JWT Access Token if old one has expired.", example = "AAB254FF67D..")
private String refreshToken;
private Authority scope;
public JwtTokenPair(String token, String refreshToken) {
this.token = token;
this.refreshToken = refreshToken;
}
}

12
application/src/main/java/org/thingsboard/server/service/security/model/token/AccessJwtToken.java

@ -15,25 +15,17 @@
*/
package org.thingsboard.server.service.security.model.token;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.jsonwebtoken.Claims;
import org.thingsboard.server.common.data.security.model.JwtToken;
public final class AccessJwtToken implements JwtToken {
private final String rawToken;
@JsonIgnore
private transient Claims claims;
protected AccessJwtToken(final String token, Claims claims) {
this.rawToken = token;
this.claims = claims;
public AccessJwtToken(String rawToken) {
this.rawToken = rawToken;
}
public String getToken() {
return this.rawToken;
}
public Claims getClaims() {
return claims;
}
}

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

@ -18,6 +18,7 @@ package org.thingsboard.server.service.security.model.token;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
@ -36,6 +37,7 @@ import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.service.security.exception.JwtExpiredTokenException;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
@ -70,39 +72,28 @@ public class JwtTokenFactory {
* Factory method for issuing new JWT Tokens.
*/
public AccessJwtToken createAccessJwtToken(SecurityUser securityUser) {
if (StringUtils.isBlank(securityUser.getEmail()))
throw new IllegalArgumentException("Cannot create JWT Token without username/email");
if (securityUser.getAuthority() == null)
if (securityUser.getAuthority() == null) {
throw new IllegalArgumentException("User doesn't have any privileges");
}
UserPrincipal principal = securityUser.getUserPrincipal();
String subject = principal.getValue();
Claims claims = Jwts.claims().setSubject(subject);
claims.put(SCOPES, securityUser.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
claims.put(USER_ID, securityUser.getId().getId().toString());
claims.put(FIRST_NAME, securityUser.getFirstName());
claims.put(LAST_NAME, securityUser.getLastName());
claims.put(ENABLED, securityUser.isEnabled());
claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
JwtBuilder jwtBuilder = setUpToken(securityUser, securityUser.getAuthorities().stream()
.map(GrantedAuthority::getAuthority).collect(Collectors.toList()), settings.getTokenExpirationTime());
jwtBuilder.claim(FIRST_NAME, securityUser.getFirstName())
.claim(LAST_NAME, securityUser.getLastName())
.claim(ENABLED, securityUser.isEnabled())
.claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
if (securityUser.getTenantId() != null) {
claims.put(TENANT_ID, securityUser.getTenantId().getId().toString());
jwtBuilder.claim(TENANT_ID, securityUser.getTenantId().getId().toString());
}
if (securityUser.getCustomerId() != null) {
claims.put(CUSTOMER_ID, securityUser.getCustomerId().getId().toString());
jwtBuilder.claim(CUSTOMER_ID, securityUser.getCustomerId().getId().toString());
}
ZonedDateTime currentTime = ZonedDateTime.now();
String token = jwtBuilder.compact();
String token = Jwts.builder()
.setClaims(claims)
.setIssuer(settings.getTokenIssuer())
.setIssuedAt(Date.from(currentTime.toInstant()))
.setExpiration(Date.from(currentTime.plusSeconds(settings.getTokenExpirationTime()).toInstant()))
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
.compact();
return new AccessJwtToken(token, claims);
return new AccessJwtToken(token);
}
public SecurityUser parseAccessJwtToken(RawAccessJwtToken rawAccessToken) {
@ -118,47 +109,40 @@ public class JwtTokenFactory {
SecurityUser securityUser = new SecurityUser(new UserId(UUID.fromString(claims.get(USER_ID, String.class))));
securityUser.setEmail(subject);
securityUser.setAuthority(Authority.parse(scopes.get(0)));
securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
securityUser.setLastName(claims.get(LAST_NAME, String.class));
securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
UserPrincipal principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
securityUser.setUserPrincipal(principal);
String tenantId = claims.get(TENANT_ID, String.class);
if (tenantId != null) {
securityUser.setTenantId(TenantId.fromUUID(UUID.fromString(tenantId)));
} else if (securityUser.getAuthority() == Authority.SYS_ADMIN) {
securityUser.setTenantId(TenantId.SYS_TENANT_ID);
}
String customerId = claims.get(CUSTOMER_ID, String.class);
if (customerId != null) {
securityUser.setCustomerId(new CustomerId(UUID.fromString(customerId)));
}
UserPrincipal principal;
if (securityUser.getAuthority() != Authority.PRE_VERIFICATION_TOKEN) {
securityUser.setFirstName(claims.get(FIRST_NAME, String.class));
securityUser.setLastName(claims.get(LAST_NAME, String.class));
securityUser.setEnabled(claims.get(ENABLED, Boolean.class));
boolean isPublic = claims.get(IS_PUBLIC, Boolean.class);
principal = new UserPrincipal(isPublic ? UserPrincipal.Type.PUBLIC_ID : UserPrincipal.Type.USER_NAME, subject);
} else {
principal = new UserPrincipal(UserPrincipal.Type.USER_NAME, subject);
}
securityUser.setUserPrincipal(principal);
return securityUser;
}
public JwtToken createRefreshToken(SecurityUser securityUser) {
if (StringUtils.isBlank(securityUser.getEmail())) {
throw new IllegalArgumentException("Cannot create JWT Token without username/email");
}
ZonedDateTime currentTime = ZonedDateTime.now();
UserPrincipal principal = securityUser.getUserPrincipal();
Claims claims = Jwts.claims().setSubject(principal.getValue());
claims.put(SCOPES, Collections.singletonList(Authority.REFRESH_TOKEN.name()));
claims.put(USER_ID, securityUser.getId().getId().toString());
claims.put(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID);
String token = Jwts.builder()
.setClaims(claims)
.setIssuer(settings.getTokenIssuer())
.setId(UUID.randomUUID().toString())
.setIssuedAt(Date.from(currentTime.toInstant()))
.setExpiration(Date.from(currentTime.plusSeconds(settings.getRefreshTokenExpTime()).toInstant()))
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey())
.compact();
String token = setUpToken(securityUser, Collections.singletonList(Authority.REFRESH_TOKEN.name()), settings.getRefreshTokenExpTime())
.claim(IS_PUBLIC, principal.getType() == UserPrincipal.Type.PUBLIC_ID)
.setId(UUID.randomUUID().toString()).compact();
return new AccessJwtToken(token, claims);
return new AccessJwtToken(token);
}
public SecurityUser parseRefreshToken(RawAccessJwtToken rawAccessToken) {
@ -180,6 +164,36 @@ public class JwtTokenFactory {
return securityUser;
}
public JwtToken createPreVerificationToken(SecurityUser user, Integer expirationTime) {
JwtBuilder jwtBuilder = setUpToken(user, Collections.singletonList(Authority.PRE_VERIFICATION_TOKEN.name()), expirationTime)
.claim(TENANT_ID, user.getTenantId().toString());
if (user.getCustomerId() != null) {
jwtBuilder.claim(CUSTOMER_ID, user.getCustomerId().toString());
}
return new AccessJwtToken(jwtBuilder.compact());
}
private JwtBuilder setUpToken(SecurityUser securityUser, List<String> scopes, long expirationTime) {
if (StringUtils.isBlank(securityUser.getEmail())) {
throw new IllegalArgumentException("Cannot create JWT Token without username/email");
}
UserPrincipal principal = securityUser.getUserPrincipal();
Claims claims = Jwts.claims().setSubject(principal.getValue());
claims.put(USER_ID, securityUser.getId().getId().toString());
claims.put(SCOPES, scopes);
ZonedDateTime currentTime = ZonedDateTime.now();
return Jwts.builder()
.setClaims(claims)
.setIssuer(settings.getTokenIssuer())
.setIssuedAt(Date.from(currentTime.toInstant()))
.setExpiration(Date.from(currentTime.plusSeconds(expirationTime).toInstant()))
.signWith(SignatureAlgorithm.HS512, settings.getTokenSigningKey());
}
public Jws<Claims> parseTokenClaims(JwtToken token) {
try {
return Jwts.parser()
@ -193,4 +207,11 @@ public class JwtTokenFactory {
throw new JwtExpiredTokenException(token, "JWT Token expired", expiredEx);
}
}
public JwtTokenPair createTokenPair(SecurityUser securityUser) {
JwtToken accessToken = createAccessJwtToken(securityUser);
JwtToken refreshToken = createRefreshToken(securityUser);
return new JwtTokenPair(accessToken.getToken(), refreshToken.getToken());
}
}

114
application/src/main/java/org/thingsboard/server/service/security/system/DefaultSystemSecurityService.java

@ -37,22 +37,30 @@ import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.rule.engine.api.MailService;
import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.common.data.security.model.UserPasswordPolicy;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.dao.user.UserServiceImpl;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.queue.util.TbCoreComponent;
import org.thingsboard.server.service.security.auth.rest.RestAuthenticationDetails;
import org.thingsboard.server.service.security.exception.UserPasswordExpiredException;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.utils.MiscUtils;
import ua_parser.Client;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
@ -65,6 +73,7 @@ import static org.thingsboard.server.common.data.CacheConstants.SECURITY_SETTING
@Service
@Slf4j
@TbCoreComponent
public class DefaultSystemSecurityService implements SystemSecurityService {
@Autowired
@ -79,6 +88,9 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
@Autowired
private MailService mailService;
@Autowired
private AuditLogService auditLogService;
@Resource
private SystemSecurityService self;
@ -122,18 +134,11 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
@Override
public void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException {
if (!encoder.matches(password, userCredentials.getPassword())) {
int failedLoginAttempts = userService.onUserLoginIncorrectCredentials(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = getSecuritySettings(tenantId);
int failedLoginAttempts = userService.increaseFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
if (securitySettings.getMaxFailedLoginAttempts() != null && securitySettings.getMaxFailedLoginAttempts() > 0) {
if (failedLoginAttempts > securitySettings.getMaxFailedLoginAttempts() && userCredentials.isEnabled()) {
userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userCredentials.getUserId(), false);
if (StringUtils.isNoneBlank(securitySettings.getUserLockoutNotificationEmail())) {
try {
mailService.sendAccountLockoutEmail(username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts());
} catch (ThingsboardException e) {
log.warn("Can't send email regarding user account [{}] lockout to provided email [{}]", username, securitySettings.getUserLockoutNotificationEmail(), e);
}
}
lockAccount(userCredentials.getUserId(), username, securitySettings.getUserLockoutNotificationEmail(), securitySettings.getMaxFailedLoginAttempts());
throw new LockedException("Authentication Failed. Username was locked due to security policy.");
}
}
@ -144,7 +149,7 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
throw new DisabledException("User is not active");
}
userService.onUserLoginSuccessful(tenantId, userCredentials.getUserId());
userService.resetFailedLoginAttempts(tenantId, userCredentials.getUserId());
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
if (isPositiveInteger(securitySettings.getPasswordPolicy().getPasswordExpirationPeriodDays())) {
@ -157,6 +162,40 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
}
}
@Override
public void validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, PlatformTwoFaSettings twoFaSettings) {
TenantId tenantId = securityUser.getTenantId();
UserId userId = securityUser.getId();
int failedVerificationAttempts;
if (!verificationSuccess) {
failedVerificationAttempts = userService.increaseFailedLoginAttempts(tenantId, userId);
} else {
userService.resetFailedLoginAttempts(tenantId, userId);
return;
}
Integer maxVerificationFailures = twoFaSettings.getMaxVerificationFailuresBeforeUserLockout();
if (maxVerificationFailures != null && maxVerificationFailures > 0
&& failedVerificationAttempts >= maxVerificationFailures) {
userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false);
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
lockAccount(userId, securityUser.getEmail(), securitySettings.getUserLockoutNotificationEmail(), maxVerificationFailures);
throw new LockedException("User account was locked due to exceeded 2FA verification attempts");
}
}
private void lockAccount(UserId userId, String username, String userLockoutNotificationEmail, Integer maxFailedLoginAttempts) {
userService.setUserCredentialsEnabled(TenantId.SYS_TENANT_ID, userId, false);
if (StringUtils.isNotBlank(userLockoutNotificationEmail)) {
try {
mailService.sendAccountLockoutEmail(username, userLockoutNotificationEmail, maxFailedLoginAttempts);
} catch (ThingsboardException e) {
log.warn("Can't send email regarding user account [{}] lockout to provided email [{}]", username, userLockoutNotificationEmail, e);
}
}
}
@Override
public void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException {
SecuritySettings securitySettings = self.getSecuritySettings(tenantId);
@ -222,6 +261,57 @@ public class DefaultSystemSecurityService implements SystemSecurityService {
return baseUrl;
}
@Override
public void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e) {
String clientAddress = "Unknown";
String browser = "Unknown";
String os = "Unknown";
String device = "Unknown";
if (authenticationDetails instanceof RestAuthenticationDetails) {
RestAuthenticationDetails details = (RestAuthenticationDetails) authenticationDetails;
clientAddress = details.getClientAddress();
if (details.getUserAgent() != null) {
Client userAgent = details.getUserAgent();
if (userAgent.userAgent != null) {
browser = userAgent.userAgent.family;
if (userAgent.userAgent.major != null) {
browser += " " + userAgent.userAgent.major;
if (userAgent.userAgent.minor != null) {
browser += "." + userAgent.userAgent.minor;
if (userAgent.userAgent.patch != null) {
browser += "." + userAgent.userAgent.patch;
}
}
}
}
if (userAgent.os != null) {
os = userAgent.os.family;
if (userAgent.os.major != null) {
os += " " + userAgent.os.major;
if (userAgent.os.minor != null) {
os += "." + userAgent.os.minor;
if (userAgent.os.patch != null) {
os += "." + userAgent.os.patch;
if (userAgent.os.patchMinor != null) {
os += "." + userAgent.os.patchMinor;
}
}
}
}
}
if (userAgent.device != null) {
device = userAgent.device.family;
}
}
}
if (actionType == ActionType.LOGIN && e == null) {
userService.setLastLoginTs(user.getTenantId(), user.getId());
}
auditLogService.logEntityAction(
user.getTenantId(), user.getCustomerId(), user.getId(),
user.getName(), user.getId(), null, actionType, e, clientAddress, browser, os, device);
}
private static boolean isPositiveInteger(Integer val) {
return val != null && val.intValue() > 0;
}

10
application/src/main/java/org/thingsboard/server/service/security/system/SystemSecurityService.java

@ -16,11 +16,15 @@
package org.thingsboard.server.service.security.system;
import org.springframework.security.core.AuthenticationException;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionType;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.UserCredentials;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.common.data.security.model.SecuritySettings;
import org.thingsboard.server.dao.exception.DataValidationException;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.service.security.model.SecurityUser;
import javax.servlet.http.HttpServletRequest;
@ -32,8 +36,12 @@ public interface SystemSecurityService {
void validateUserCredentials(TenantId tenantId, UserCredentials userCredentials, String username, String password) throws AuthenticationException;
void validateTwoFaVerification(SecurityUser securityUser, boolean verificationSuccess, PlatformTwoFaSettings twoFaSettings);
void validatePassword(TenantId tenantId, String password, UserCredentials userCredentials) throws DataValidationException;
String getBaseUrl(TenantId tenantId, CustomerId customerId, HttpServletRequest httpServletRequest);
void logLoginAction(User user, Object authenticationDetails, ActionType actionType, Exception e);
}

6
application/src/main/java/org/thingsboard/server/service/sms/DefaultSmsService.java

@ -29,7 +29,6 @@ import org.thingsboard.server.common.data.AdminSettings;
import org.thingsboard.server.common.data.ApiUsageRecordKey;
import org.thingsboard.server.common.data.exception.ThingsboardErrorCode;
import org.thingsboard.server.common.data.exception.ThingsboardException;
import org.thingsboard.server.common.data.id.EntityId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.dao.settings.AdminSettingsService;
import org.thingsboard.common.util.JacksonUtil;
@ -124,6 +123,11 @@ public class DefaultSmsService implements SmsService {
testSmsSender.destroy();
}
@Override
public boolean isConfigured(TenantId tenantId) {
return smsSender != null;
}
private int sendSms(SmsSender smsSender, String numberTo, String message) throws ThingsboardException {
try {
return smsSender.sendSms(numberTo, message);

3
application/src/main/resources/i18n/messages.properties

@ -4,4 +4,5 @@ account.activated.subject=Thingsboard - your account has been activated
reset.password.subject=Thingsboard - Password reset has been requested
password.was.reset.subject=Thingsboard - your account password has been reset
account.lockout.subject=Thingsboard - User account has been lockout
api.usage.state=Thingsboard - Account limits
api.usage.state=Thingsboard - Account limits
2fa.verification.code.subject=ThingsBoard - 2FA verification code

117
application/src/main/resources/templates/2fa.verification.code.ftl

@ -0,0 +1,117 @@
<#--
Copyright © 2016-2022 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.
-->
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>Email verification code</title>
<style type="text/css">
img {
max-width: 100%;
}
body {
-webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em;
}
body {
background-color: #f6f6f6;
}
@media only screen and (max-width: 640px) {
body {
padding: 0 !important;
}
h2 {
font-weight: 800 !important;
margin: 0 0 8px !important;
}
h2 {
font-size: 18px !important;
}
.container {
padding: 0 !important;
width: 100% !important;
}
.content {
padding: 0 !important;
}
.content-wrap {
padding: 10px !important;
}
}
</style>
</head>
<body itemscope itemtype="http://schema.org/EmailMessage"
style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; width: 100% !important; height: 100%; line-height: 1.6em; background-color: #f6f6f6; margin: 0;"
bgcolor="#f6f6f6">
<table class="body-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
<td class="container" width="600" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; margin: 0 auto;" valign="top">
<div class="content" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 20px;">
<table class="main" width="100%" cellpadding="0" cellspacing="0" itemprop="action" itemscope itemtype="http://schema.org/ConfirmAction" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff"><tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;"><td class="content-wrap" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 20px;" valign="top">
<meta itemprop="name" content="Confirm Email" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" /><table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0;" valign="top" align="center">
<h2 style="margin-bottom: 0;">Your verification code:</h2>
</td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; color: #348eda; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top" align="center">
<h2 style="margin-top: 8px;">${code}</h2>
</td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 8px;" valign="top">
Please verify your access using the code above.
</td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 8px;" valign="top">
This code will expire in ${expirationTimeSeconds} seconds.
</td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
If you didn't request this code, you can ignore this email.
</td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
&mdash; The Thingsboard
</td>
</tr></table></td>
</tr>
</table>
<div class="footer" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
<table width="100%" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="aligncenter content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; color: #999; text-align: center; margin: 0; padding: 0 0 20px;" align="center" valign="top">This email was sent to <a href="mailto:${targetEmail}" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">${targetEmail}</a> by Thingsboard.</td>
</tr>
</table>
</div>
</div>
</td>
<td style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
</tr>
</table>
</body>
</html>

2
application/src/main/resources/templates/account.lockout.ftl

@ -88,7 +88,7 @@ background-color: #f6f6f6;
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
<td class="content-block" style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
Thingsboard user account ${lockoutAccount} has been lockout due to failed credentials were provided more than ${maxFailedLoginAttempts} times.
Thingsboard user account ${lockoutAccount} has been locked out due to multiple authentication failures (more than ${maxFailedLoginAttempts}).
</td>
</tr>
<tr style="font-family: 'Helvetica Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">

3
application/src/main/resources/thingsboard.yml

@ -439,6 +439,9 @@ cache:
autoCommitSettings:
timeToLiveInMinutes: "${CACHE_SPECS_AUTO_COMMIT_SETTINGS_TTL:1440}"
maxSize: "${CACHE_SPECS_AUTO_COMMIT_SETTINGS_MAX_SIZE:10000}"
twoFaVerificationCodes:
timeToLiveInMinutes: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_TTL:60}"
maxSize: "${CACHE_SPECS_TWO_FA_VERIFICATION_CODES_MAX_SIZE:100000}"
redis:
# standalone or cluster

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

@ -71,6 +71,7 @@ import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.HasId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UUIDBased;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.page.PageData;
import org.thingsboard.server.common.data.page.PageLink;
import org.thingsboard.server.common.data.page.TimePageLink;
@ -135,6 +136,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
protected String username;
protected TenantId tenantId;
protected UserId tenantAdminUserId;
protected CustomerId customerId;
@SuppressWarnings("rawtypes")
@ -198,7 +200,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
tenantAdmin.setTenantId(tenantId);
tenantAdmin.setEmail(TENANT_ADMIN_EMAIL);
createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD);
tenantAdmin = createUserAndLogin(tenantAdmin, TENANT_ADMIN_PASSWORD);
tenantAdminUserId = tenantAdmin.getId();
Customer customer = new Customer();
customer.setTitle("Customer");
@ -632,6 +635,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest {
return mapper.readerFor(type).readValue(content);
}
protected String getErrorMessage(ResultActions result) throws Exception {
return readResponse(result, JsonNode.class).get("message").asText();
}
public class IdComparator<D extends HasId> implements Comparator<D> {
@Override
public int compare(D o1, D o2) {

470
application/src/test/java/org/thingsboard/server/controller/TwoFactorAuthConfigTest.java

@ -0,0 +1,470 @@
/**
* Copyright © 2016-2022 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.controller;
import org.jboss.aerogear.security.otp.Totp;
import org.jboss.aerogear.security.otp.api.Base32;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.springframework.cache.CacheManager;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.CacheConstants;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.auth.mfa.provider.impl.OtpBasedTwoFaProvider;
import org.thingsboard.server.service.security.auth.mfa.provider.impl.TotpTwoFaProvider;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public abstract class TwoFactorAuthConfigTest extends AbstractControllerTest {
@SpyBean
private TotpTwoFaProvider totpTwoFactorAuthProvider;
@MockBean
private SmsService smsService;
@Autowired
private CacheManager cacheManager;
@Autowired
private TwoFaConfigManager twoFaConfigManager;
@SpyBean
private TwoFactorAuthService twoFactorAuthService;
@Before
public void beforeEach() throws Exception {
doNothing().when(twoFactorAuthService).checkProvider(any(), any());
loginSysAdmin();
}
@After
public void afterEach() {
twoFaConfigManager.deletePlatformTwoFaSettings(TenantId.SYS_TENANT_ID);
twoFaConfigManager.deletePlatformTwoFaSettings(tenantId);
}
@Test
public void testSavePlatformTwoFaSettings() throws Exception {
loginSysAdmin();
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${code}");
smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig));
twoFaSettings.setVerificationCodeCheckRateLimit("3:900");
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
twoFaSettings.setTotalAllowedTimeForVerification(3600);
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
PlatformTwoFaSettings savedTwoFaSettings = readResponse(doGet("/api/2fa/settings").andExpect(status().isOk()), PlatformTwoFaSettings.class);
assertThat(savedTwoFaSettings.getProviders()).hasSize(2);
assertThat(savedTwoFaSettings.getProviders()).contains(totpTwoFaProviderConfig, smsTwoFaProviderConfig);
}
@Test
public void testSavePlatformTwoFaSettings_validationError() throws Exception {
loginSysAdmin();
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Collections.emptyList());
twoFaSettings.setVerificationCodeCheckRateLimit("0:12");
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(-1);
twoFaSettings.setTotalAllowedTimeForVerification(0);
String errorMessage = getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).contains(
"verification code check rate limit configuration is invalid",
"maximum number of verification failure before user lockout must be positive",
"total amount of time allotted for verification must be greater than or equal 60"
);
}
@Test
public void testSaveTotpTwoFaProviderConfig_validationError() throws Exception {
TotpTwoFaProviderConfig invalidTotpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
invalidTotpTwoFaProviderConfig.setIssuerName(" ");
String errorResponse = savePlatformTwoFaSettingsAndGetError(invalidTotpTwoFaProviderConfig);
assertThat(errorResponse).containsIgnoringCase("issuer name must not be blank");
}
@Test
public void testSaveSmsTwoFaProviderConfig_validationError() throws Exception {
SmsTwoFaProviderConfig invalidSmsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate("does not contain verification code");
invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(60);
String errorResponse = savePlatformTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig);
assertThat(errorResponse).containsIgnoringCase("must contain verification code");
invalidSmsTwoFaProviderConfig.setSmsVerificationMessageTemplate(null);
invalidSmsTwoFaProviderConfig.setVerificationCodeLifetime(0);
errorResponse = savePlatformTwoFaSettingsAndGetError(invalidSmsTwoFaProviderConfig);
assertThat(errorResponse).containsIgnoringCase("verification message template is required");
assertThat(errorResponse).containsIgnoringCase("verification code lifetime is required");
}
private String savePlatformTwoFaSettingsAndGetError(TwoFaProviderConfig invalidTwoFaProviderConfig) throws Exception {
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Collections.singletonList(invalidTwoFaProviderConfig));
return getErrorMessage(doPost("/api/2fa/settings", twoFaSettings)
.andExpect(status().isBadRequest()));
}
@Test
public void testSaveTwoFaAccountConfig_providerNotConfigured() throws Exception {
configureSmsTwoFaProvider("${code}");
loginTenantAdmin();
TwoFaProviderType notConfiguredProviderType = TwoFaProviderType.TOTP;
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/generate?providerType=" + notConfiguredProviderType)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("provider is not configured");
TotpTwoFaAccountConfig notConfiguredProviderAccountConfig = new TotpTwoFaAccountConfig();
notConfiguredProviderAccountConfig.setAuthUrl("otpauth://totp/aba:aba?issuer=aba&secret=ABA");
errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", notConfiguredProviderAccountConfig));
assertThat(errorMessage).containsIgnoringCase("provider is not configured");
}
@Test
public void testGenerateTotpTwoFaAccountConfig() throws Exception {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin();
assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), String.class)).isNullOrEmpty();
generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
}
@Test
public void testSubmitTotpTwoFaAccountConfig() throws Exception {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin();
TotpTwoFaAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
doPost("/api/2fa/account/config/submit", generatedTotpTwoFaAccountConfig).andExpect(status().isOk());
verify(totpTwoFactorAuthProvider).prepareVerificationCode(argThat(user -> user.getEmail().equals(TENANT_ADMIN_EMAIL)),
eq(totpTwoFaProviderConfig), eq(generatedTotpTwoFaAccountConfig));
}
@Test
public void testSubmitTotpTwoFaAccountConfig_validationError() throws Exception {
configureTotpTwoFaProvider();
loginTenantAdmin();
TotpTwoFaAccountConfig totpTwoFaAccountConfig = new TotpTwoFaAccountConfig();
totpTwoFaAccountConfig.setAuthUrl(null);
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("otp auth url cannot be blank");
totpTwoFaAccountConfig.setAuthUrl("otpauth://totp/T B: aba");
errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("otp auth url is invalid");
totpTwoFaAccountConfig.setAuthUrl("otpauth://totp/ThingsBoard%20(Tenant):tenant@thingsboard.org?issuer=ThingsBoard+%28Tenant%29&secret=FUNBIM3CXFNNGQR6ZIPVWHP65PPFWDII");
doPost("/api/2fa/account/config/submit", totpTwoFaAccountConfig)
.andExpect(status().isOk());
}
@Test
public void testVerifyAndSaveTotpTwoFaAccountConfig() throws Exception {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin();
TotpTwoFaAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
generatedTotpTwoFaAccountConfig.setUseByDefault(true);
String secret = UriComponentsBuilder.fromUriString(generatedTotpTwoFaAccountConfig.getAuthUrl()).build()
.getQueryParams().getFirst("secret");
String correctVerificationCode = new Totp(secret).now();
doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, generatedTotpTwoFaAccountConfig)
.andExpect(status().isOk());
AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class);
assertThat(accountTwoFaSettings.getConfigs()).size().isOne();
TwoFaAccountConfig twoFaAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.TOTP);
assertThat(twoFaAccountConfig).isEqualTo(generatedTotpTwoFaAccountConfig);
}
@Test
public void testVerifyAndSaveTotpTwoFaAccountConfig_incorrectVerificationCode() throws Exception {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = configureTotpTwoFaProvider();
loginTenantAdmin();
TotpTwoFaAccountConfig generatedTotpTwoFaAccountConfig = generateTotpTwoFaAccountConfig(totpTwoFaProviderConfig);
String incorrectVerificationCode = "100000";
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + incorrectVerificationCode, generatedTotpTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
}
private TotpTwoFaAccountConfig generateTotpTwoFaAccountConfig(TotpTwoFaProviderConfig totpTwoFaProviderConfig) throws Exception {
TwoFaAccountConfig generatedTwoFaAccountConfig = readResponse(doPost("/api/2fa/account/config/generate?providerType=TOTP")
.andExpect(status().isOk()), TwoFaAccountConfig.class);
assertThat(generatedTwoFaAccountConfig).isInstanceOf(TotpTwoFaAccountConfig.class);
assertThat(((TotpTwoFaAccountConfig) generatedTwoFaAccountConfig)).satisfies(accountConfig -> {
UriComponents otpAuthUrl = UriComponentsBuilder.fromUriString(accountConfig.getAuthUrl()).build();
assertThat(otpAuthUrl.getScheme()).isEqualTo("otpauth");
assertThat(otpAuthUrl.getHost()).isEqualTo("totp");
assertThat(otpAuthUrl.getQueryParams().getFirst("issuer")).isEqualTo(totpTwoFaProviderConfig.getIssuerName());
assertThat(otpAuthUrl.getPath()).isEqualTo("/%s:%s", totpTwoFaProviderConfig.getIssuerName(), TENANT_ADMIN_EMAIL);
assertThat(otpAuthUrl.getQueryParams().getFirst("secret")).satisfies(secretKey -> {
assertDoesNotThrow(() -> Base32.decode(secretKey));
});
});
return (TotpTwoFaAccountConfig) generatedTwoFaAccountConfig;
}
@Test
public void testGetTwoFaAccountConfig_whenProviderNotConfigured() throws Exception {
testVerifyAndSaveTotpTwoFaAccountConfig();
assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()),
AccountTwoFaSettings.class).getConfigs()).isNotEmpty();
loginSysAdmin();
saveProvidersConfigs();
loginTenantAdmin();
assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs())
.isEmpty();
}
@Test
public void testGenerateSmsTwoFaAccountConfig() throws Exception {
configureSmsTwoFaProvider("${code}");
doPost("/api/2fa/account/config/generate?providerType=SMS")
.andExpect(status().isOk());
}
@Test
public void testSubmitSmsTwoFaAccountConfig() throws Exception {
String verificationMessageTemplate = "Here is your verification code: ${code}";
configureSmsTwoFaProvider(verificationMessageTemplate);
loginTenantAdmin();
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38054159785");
doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig).andExpect(status().isOk());
String verificationCode = cacheManager.getCache(CacheConstants.TWO_FA_VERIFICATION_CODES_CACHE)
.get(tenantAdminUserId, OtpBasedTwoFaProvider.Otp.class).getValue();
verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> {
return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber());
}), eq("Here is your verification code: " + verificationCode));
}
@Test
public void testSubmitSmsTwoFaAccountConfig_validationError() throws Exception {
configureSmsTwoFaProvider("${code}");
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
String blankPhoneNumber = "";
smsTwoFaAccountConfig.setPhoneNumber(blankPhoneNumber);
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("phone number cannot be blank");
String nonE164PhoneNumber = "8754868";
smsTwoFaAccountConfig.setPhoneNumber(nonE164PhoneNumber);
errorMessage = getErrorMessage(doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("phone number is not of E.164 format");
}
@Test
public void testVerifyAndSaveSmsTwoFaAccountConfig() throws Exception {
configureSmsTwoFaProvider("${code}");
loginTenantAdmin();
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38051889445");
smsTwoFaAccountConfig.setUseByDefault(true);
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
doPost("/api/2fa/account/config/submit", smsTwoFaAccountConfig).andExpect(status().isOk());
verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> {
return phoneNumbers[0].equals(smsTwoFaAccountConfig.getPhoneNumber());
}), verificationCodeCaptor.capture());
String correctVerificationCode = verificationCodeCaptor.getValue();
doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, smsTwoFaAccountConfig)
.andExpect(status().isOk());
AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class);
TwoFaAccountConfig accountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS);
assertThat(accountConfig).isEqualTo(smsTwoFaAccountConfig);
}
@Test
public void testVerifyAndSaveSmsTwoFaAccountConfig_incorrectVerificationCode() throws Exception {
configureSmsTwoFaProvider("${code}");
loginTenantAdmin();
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38051889445");
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=100000", smsTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
}
@Test
public void testVerifyAndSaveSmsTwoFaAccountConfig_differentAccountConfigs() throws Exception {
configureSmsTwoFaProvider("${code}");
loginTenantAdmin();
SmsTwoFaAccountConfig initialSmsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
initialSmsTwoFaAccountConfig.setPhoneNumber("+38051889445");
initialSmsTwoFaAccountConfig.setUseByDefault(true);
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
doPost("/api/2fa/account/config/submit", initialSmsTwoFaAccountConfig).andExpect(status().isOk());
verify(smsService).sendSms(eq(tenantId), any(), argThat(phoneNumbers -> {
return phoneNumbers[0].equals(initialSmsTwoFaAccountConfig.getPhoneNumber());
}), verificationCodeCaptor.capture());
String correctVerificationCode = verificationCodeCaptor.getValue();
SmsTwoFaAccountConfig anotherSmsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
anotherSmsTwoFaAccountConfig.setPhoneNumber("+38111111111");
String errorMessage = getErrorMessage(doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, anotherSmsTwoFaAccountConfig)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
doPost("/api/2fa/account/config?verificationCode=" + correctVerificationCode, initialSmsTwoFaAccountConfig)
.andExpect(status().isOk());
AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class);
TwoFaAccountConfig accountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS);
assertThat(accountConfig).isEqualTo(initialSmsTwoFaAccountConfig);
}
private TotpTwoFaProviderConfig configureTotpTwoFaProvider() throws Exception {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
saveProvidersConfigs(totpTwoFaProviderConfig);
return totpTwoFaProviderConfig;
}
private SmsTwoFaProviderConfig configureSmsTwoFaProvider(String verificationMessageTemplate) throws Exception {
SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate(verificationMessageTemplate);
smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
saveProvidersConfigs(smsTwoFaProviderConfig);
return smsTwoFaProviderConfig;
}
private void saveProvidersConfigs(TwoFaProviderConfig... providerConfigs) throws Exception {
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Arrays.stream(providerConfigs).collect(Collectors.toList()));
doPost("/api/2fa/settings", twoFaSettings).andExpect(status().isOk());
}
@Test
public void testIsTwoFaEnabled() throws Exception {
configureSmsTwoFaProvider("${code}");
SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig();
accountConfig.setPhoneNumber("+38050505050");
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig);
assertThat(twoFactorAuthService.isTwoFaEnabled(tenantId, tenantAdminUserId)).isTrue();
}
@Test
public void testDeleteTwoFaAccountConfig() throws Exception {
configureSmsTwoFaProvider("${code}");
SmsTwoFaAccountConfig accountConfig = new SmsTwoFaAccountConfig();
accountConfig.setPhoneNumber("+38050505050");
loginTenantAdmin();
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, tenantAdminUserId, accountConfig);
AccountTwoFaSettings accountTwoFaSettings = readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class);
TwoFaAccountConfig savedAccountConfig = accountTwoFaSettings.getConfigs().get(TwoFaProviderType.SMS);
assertThat(savedAccountConfig).isEqualTo(accountConfig);
doDelete("/api/2fa/account/config?providerType=SMS").andExpect(status().isOk());
assertThat(readResponse(doGet("/api/2fa/account/settings").andExpect(status().isOk()), AccountTwoFaSettings.class).getConfigs())
.doesNotContainKey(TwoFaProviderType.SMS);
}
}

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

@ -0,0 +1,441 @@
/**
* Copyright © 2016-2022 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.controller;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.JsonNode;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.jboss.aerogear.security.otp.Totp;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.test.mock.mockito.SpyBean;
import org.thingsboard.rule.engine.api.SmsService;
import org.thingsboard.server.common.data.User;
import org.thingsboard.server.common.data.audit.ActionStatus;
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.page.PageLink;
import org.thingsboard.server.common.data.page.SortOrder;
import org.thingsboard.server.common.data.page.TimePageLink;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.mfa.PlatformTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.EmailTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.SmsTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.account.TotpTwoFaAccountConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.EmailTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.SmsTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TotpTwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import org.thingsboard.server.dao.audit.AuditLogService;
import org.thingsboard.server.dao.user.UserService;
import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService;
import org.thingsboard.server.service.security.auth.mfa.config.TwoFaConfigManager;
import org.thingsboard.server.service.security.auth.rest.LoginRequest;
import org.thingsboard.server.service.security.model.JwtTokenPair;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
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.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
public abstract class TwoFactorAuthTest extends AbstractControllerTest {
@Autowired
private TwoFaConfigManager twoFaConfigManager;
@SpyBean
private TwoFactorAuthService twoFactorAuthService;
@MockBean
private SmsService smsService;
@Autowired
private AuditLogService auditLogService;
@Autowired
private UserService userService;
private User user;
private String username;
private String password;
@Before
public void beforeEach() throws Exception {
username = "mfa@tb.io";
password = "psswrd";
user = new User();
user.setAuthority(Authority.TENANT_ADMIN);
user.setEmail(username);
user.setTenantId(tenantId);
loginSysAdmin();
user = createUser(user, password);
doNothing().when(twoFactorAuthService).checkProvider(any(), any());
}
@After
public void afterEach() {
twoFaConfigManager.deletePlatformTwoFaSettings(tenantId);
twoFaConfigManager.deletePlatformTwoFaSettings(TenantId.SYS_TENANT_ID);
}
@Test
public void testTwoFa_totp() throws Exception {
TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa();
logInWithPreVerificationToken(username, password);
doPost("/api/auth/2fa/verification/send?providerType=TOTP")
.andExpect(status().isOk());
String correctVerificationCode = getCorrectTotp(totpTwoFaAccountConfig);
JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + correctVerificationCode)
.andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenPair, username);
User currentUser = readResponse(doGet("/api/auth/user")
.andExpect(status().isOk()), User.class);
assertThat(currentUser.getId()).isEqualTo(user.getId());
}
@Test
public void testTwoFa_sms() throws Exception {
configureSmsTwoFa();
logInWithPreVerificationToken(username, password);
doPost("/api/auth/2fa/verification/send?providerType=SMS")
.andExpect(status().isOk());
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture());
String correctVerificationCode = verificationCodeCaptor.getValue();
JsonNode tokenPair = readResponse(doPost("/api/auth/2fa/verification/check?providerType=SMS&verificationCode=" + correctVerificationCode)
.andExpect(status().isOk()), JsonNode.class);
validateAndSetJwtToken(tokenPair, username);
User currentUser = readResponse(doGet("/api/auth/user")
.andExpect(status().isOk()), User.class);
assertThat(currentUser.getId()).isEqualTo(user.getId());
}
@Test
public void testTwoFaPreVerificationTokenLifetime() throws Exception {
configureTotpTwoFa(twoFaSettings -> {
twoFaSettings.setTotalAllowedTimeForVerification(65);
});
logInWithPreVerificationToken(username, password);
await("expiration of the pre-verification token")
.atLeast(Duration.ofSeconds(30).plusMillis(500))
.atMost(Duration.ofSeconds(70))
.untilAsserted(() -> {
doPost("/api/auth/2fa/verification/send?providerType=TOTP")
.andExpect(status().isUnauthorized());
});
}
@Test
public void testCheckVerificationCode_userBlocked() throws Exception {
configureTotpTwoFa(twoFaSettings -> {
twoFaSettings.setMaxVerificationFailuresBeforeUserLockout(10);
});
logInWithPreVerificationToken(username, password);
Stream.generate(() -> RandomStringUtils.randomNumeric(6))
.limit(9)
.forEach(incorrectVerificationCode -> {
try {
String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + incorrectVerificationCode)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
} catch (Exception e) {
fail();
}
});
String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + RandomStringUtils.randomNumeric(6))
.andExpect(status().isUnauthorized()));
assertThat(errorMessage).containsIgnoringCase("account was locked due to exceeded 2fa verification attempts");
errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + RandomStringUtils.randomNumeric(6))
.andExpect(status().isUnauthorized()));
assertThat(errorMessage).containsIgnoringCase("user is disabled");
}
@Test
public void testSendVerificationCode_rateLimit() throws Exception {
configureTotpTwoFa(twoFaSettings -> {
twoFaSettings.setMinVerificationCodeSendPeriod(10);
});
logInWithPreVerificationToken(username, password);
doPost("/api/auth/2fa/verification/send?providerType=TOTP")
.andExpect(status().isOk());
String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/send?providerType=TOTP")
.andExpect(status().isTooManyRequests()));
assertThat(rateLimitExceededError).containsIgnoringCase("too many requests");
await("verification code sending rate limit resetting")
.atLeast(Duration.ofSeconds(8))
.atMost(Duration.ofSeconds(12))
.untilAsserted(() -> {
doPost("/api/auth/2fa/verification/send?providerType=TOTP")
.andExpect(status().isOk());
});
}
@Test
public void testCheckVerificationCode_rateLimit() throws Exception {
TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa(twoFaSettings -> {
twoFaSettings.setVerificationCodeCheckRateLimit("3:10");
});
logInWithPreVerificationToken(username, password);
for (int i = 0; i < 3; i++) {
String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect")
.andExpect(status().isBadRequest()));
assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect");
}
String rateLimitExceededError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect")
.andExpect(status().isTooManyRequests()));
assertThat(rateLimitExceededError).containsIgnoringCase("too many requests");
await("verification code checking rate limit resetting")
.atLeast(Duration.ofSeconds(8))
.atMost(Duration.ofSeconds(12))
.untilAsserted(() -> {
String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect")
.andExpect(status().isBadRequest()));
assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect");
});
doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig))
.andExpect(status().isOk());
}
@Test
public void testCheckVerificationCode_invalidVerificationCode() throws Exception {
configureTotpTwoFa();
logInWithPreVerificationToken(username, password);
for (String invalidVerificationCode : new String[]{"1234567", "ab1212", "12311 ", "oewkriwejqf"}) {
String errorMessage = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + invalidVerificationCode)
.andExpect(status().isBadRequest()));
assertThat(errorMessage).containsIgnoringCase("verification code is incorrect");
}
}
@Test
public void testCheckVerificationCode_codeExpiration() throws Exception {
configureSmsTwoFa(smsTwoFaProviderConfig -> {
smsTwoFaProviderConfig.setVerificationCodeLifetime(10);
});
logInWithPreVerificationToken(username, password);
ArgumentCaptor<String> verificationCodeCaptor = ArgumentCaptor.forClass(String.class);
doPost("/api/auth/2fa/verification/send?providerType=SMS").andExpect(status().isOk());
verify(smsService).sendSms(eq(tenantId), any(), any(), verificationCodeCaptor.capture());
String correctVerificationCode = verificationCodeCaptor.getValue();
await("verification code expiration")
.pollDelay(10, TimeUnit.SECONDS)
.atLeast(10, TimeUnit.SECONDS)
.atMost(12, TimeUnit.SECONDS)
.untilAsserted(() -> {
String incorrectVerificationCodeError = getErrorMessage(doPost("/api/auth/2fa/verification/check?providerType=SMS&verificationCode=" + correctVerificationCode)
.andExpect(status().isBadRequest()));
assertThat(incorrectVerificationCodeError).containsIgnoringCase("verification code is incorrect");
});
}
@Test
public void testTwoFa_logLoginAction() throws Exception {
TotpTwoFaAccountConfig totpTwoFaAccountConfig = configureTotpTwoFa();
logInWithPreVerificationToken(username, password);
await("async audit log saving").during(1, TimeUnit.SECONDS);
assertThat(getLogInAuditLogs()).isEmpty();
assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo()
.get("lastLoginTs")).isNull();
doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=incorrect")
.andExpect(status().isBadRequest());
await("async audit log saving").atMost(1, TimeUnit.SECONDS)
.until(() -> getLogInAuditLogs().size() == 1);
assertThat(getLogInAuditLogs().get(0)).satisfies(failedLogInAuditLog -> {
assertThat(failedLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.FAILURE);
assertThat(failedLogInAuditLog.getActionFailureDetails()).containsIgnoringCase("verification code is incorrect");
assertThat(failedLogInAuditLog.getUserName()).isEqualTo(username);
});
doPost("/api/auth/2fa/verification/check?providerType=TOTP&verificationCode=" + getCorrectTotp(totpTwoFaAccountConfig))
.andExpect(status().isOk());
await("async audit log saving").atMost(1, TimeUnit.SECONDS)
.until(() -> getLogInAuditLogs().size() == 2);
assertThat(getLogInAuditLogs().get(0)).satisfies(successfulLogInAuditLog -> {
assertThat(successfulLogInAuditLog.getActionStatus()).isEqualTo(ActionStatus.SUCCESS);
assertThat(successfulLogInAuditLog.getUserName()).isEqualTo(username);
});
assertThat(userService.findUserById(tenantId, user.getId()).getAdditionalInfo()
.get("lastLoginTs").asLong())
.isGreaterThan(System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(3));
}
private List<AuditLog> getLogInAuditLogs() {
return auditLogService.findAuditLogsByTenantIdAndUserId(tenantId, user.getId(), List.of(ActionType.LOGIN),
new TimePageLink(new PageLink(10, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC)), 0L, System.currentTimeMillis())).getData();
}
@Test
public void testAuthWithoutTwoFaAccountConfig() throws ThingsboardException {
configureTotpTwoFa();
twoFaConfigManager.deleteTwoFaAccountConfig(tenantId, user.getId(), TwoFaProviderType.TOTP);
assertDoesNotThrow(() -> {
login(username, password);
});
}
@Test
public void testTwoFa_multipleProviders() throws Exception {
PlatformTwoFaSettings platformTwoFaSettings = new PlatformTwoFaSettings();
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("TB");
SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${code}");
EmailTwoFaProviderConfig emailTwoFaProviderConfig = new EmailTwoFaProviderConfig();
emailTwoFaProviderConfig.setVerificationCodeLifetime(60);
platformTwoFaSettings.setProviders(List.of(totpTwoFaProviderConfig, smsTwoFaProviderConfig, emailTwoFaProviderConfig));
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, platformTwoFaSettings);
User twoFaUser = new User();
twoFaUser.setAuthority(Authority.TENANT_ADMIN);
twoFaUser.setTenantId(tenantId);
twoFaUser.setEmail("2fa@thingsboard.org");
twoFaUser = createUserAndLogin(twoFaUser, "12345678");
TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(twoFaUser, TwoFaProviderType.TOTP);
totpTwoFaAccountConfig.setUseByDefault(true);
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), totpTwoFaAccountConfig);
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38012312322");
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), smsTwoFaAccountConfig);
EmailTwoFaAccountConfig emailTwoFaAccountConfig = new EmailTwoFaAccountConfig();
emailTwoFaAccountConfig.setEmail(twoFaUser.getEmail());
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, twoFaUser.getId(), emailTwoFaAccountConfig);
logInWithPreVerificationToken(twoFaUser.getEmail(), "12345678");
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));
assertThat(providersInfos).size().isEqualTo(3);
assertThat(providersInfos).containsKey(TwoFaProviderType.TOTP);
assertThat(providersInfos.get(TwoFaProviderType.TOTP).isDefault()).isTrue();
assertThat(providersInfos).containsKey(TwoFaProviderType.SMS);
assertThat(providersInfos.get(TwoFaProviderType.SMS).isDefault()).isFalse();
assertThat(providersInfos).containsKey(TwoFaProviderType.EMAIL);
assertThat(providersInfos.get(TwoFaProviderType.EMAIL).isDefault()).isFalse();
}
private void logInWithPreVerificationToken(String username, String password) throws Exception {
LoginRequest loginRequest = new LoginRequest(username, password);
JwtTokenPair response = readResponse(doPost("/api/auth/login", loginRequest).andExpect(status().isOk()), JwtTokenPair.class);
assertThat(response.getToken()).isNotNull();
assertThat(response.getRefreshToken()).isNull();
assertThat(response.getScope()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN);
this.token = response.getToken();
}
private TotpTwoFaAccountConfig configureTotpTwoFa(Consumer<PlatformTwoFaSettings>... customizer) throws ThingsboardException {
TotpTwoFaProviderConfig totpTwoFaProviderConfig = new TotpTwoFaProviderConfig();
totpTwoFaProviderConfig.setIssuerName("tb");
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{totpTwoFaProviderConfig}).collect(Collectors.toList()));
Arrays.stream(customizer).forEach(c -> c.accept(twoFaSettings));
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
TotpTwoFaAccountConfig totpTwoFaAccountConfig = (TotpTwoFaAccountConfig) twoFactorAuthService.generateNewAccountConfig(user, TwoFaProviderType.TOTP);
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), totpTwoFaAccountConfig);
return totpTwoFaAccountConfig;
}
private SmsTwoFaAccountConfig configureSmsTwoFa(Consumer<SmsTwoFaProviderConfig>... customizer) throws ThingsboardException {
SmsTwoFaProviderConfig smsTwoFaProviderConfig = new SmsTwoFaProviderConfig();
smsTwoFaProviderConfig.setVerificationCodeLifetime(60);
smsTwoFaProviderConfig.setSmsVerificationMessageTemplate("${code}");
Arrays.stream(customizer).forEach(c -> c.accept(smsTwoFaProviderConfig));
PlatformTwoFaSettings twoFaSettings = new PlatformTwoFaSettings();
twoFaSettings.setProviders(Arrays.stream(new TwoFaProviderConfig[]{smsTwoFaProviderConfig}).collect(Collectors.toList()));
twoFaConfigManager.savePlatformTwoFaSettings(TenantId.SYS_TENANT_ID, twoFaSettings);
SmsTwoFaAccountConfig smsTwoFaAccountConfig = new SmsTwoFaAccountConfig();
smsTwoFaAccountConfig.setPhoneNumber("+38050505050");
twoFaConfigManager.saveTwoFaAccountConfig(tenantId, user.getId(), smsTwoFaAccountConfig);
return smsTwoFaAccountConfig;
}
private String getCorrectTotp(TotpTwoFaAccountConfig totpTwoFaAccountConfig) {
String secret = StringUtils.substringAfterLast(totpTwoFaAccountConfig.getAuthUrl(), "secret=");
return new Totp(secret).now();
}
}

23
application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthConfigSqlTest.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2022 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.controller.sql;
import org.thingsboard.server.dao.service.DaoSqlTest;
import org.thingsboard.server.controller.TwoFactorAuthConfigTest;
@DaoSqlTest
public class TwoFactorAuthConfigSqlTest extends TwoFactorAuthConfigTest {
}

23
application/src/test/java/org/thingsboard/server/controller/sql/TwoFactorAuthSqlTest.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2022 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.controller.sql;
import org.thingsboard.server.controller.TwoFactorAuthTest;
import org.thingsboard.server.dao.service.DaoSqlTest;
@DaoSqlTest
public class TwoFactorAuthSqlTest extends TwoFactorAuthTest {
}

165
application/src/test/java/org/thingsboard/server/service/security/auth/JwtTokenFactoryTest.java

@ -0,0 +1,165 @@
/**
* Copyright © 2016-2022 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.service.security.auth;
import io.jsonwebtoken.Claims;
import org.junit.BeforeClass;
import org.junit.Test;
import org.thingsboard.server.common.data.id.CustomerId;
import org.thingsboard.server.common.data.id.TenantId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.Authority;
import org.thingsboard.server.common.data.security.model.JwtToken;
import org.thingsboard.server.config.JwtSettings;
import org.thingsboard.server.service.security.model.SecurityUser;
import org.thingsboard.server.service.security.model.UserPrincipal;
import org.thingsboard.server.service.security.model.token.AccessJwtToken;
import org.thingsboard.server.service.security.model.token.JwtTokenFactory;
import org.thingsboard.server.service.security.model.token.RawAccessJwtToken;
import java.util.Calendar;
import java.util.Date;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
public class JwtTokenFactoryTest {
private static JwtTokenFactory tokenFactory;
private static JwtSettings jwtSettings;
@BeforeClass
public static void beforeAll() {
jwtSettings = new JwtSettings();
jwtSettings.setTokenIssuer("tb");
jwtSettings.setTokenSigningKey("abewafaf");
jwtSettings.setTokenExpirationTime((int) TimeUnit.HOURS.toSeconds(2));
jwtSettings.setRefreshTokenExpTime((int) TimeUnit.DAYS.toSeconds(7));
tokenFactory = new JwtTokenFactory(jwtSettings);
}
@Test
public void testCreateAndParseAccessJwtToken() {
SecurityUser securityUser = new SecurityUser();
securityUser.setId(new UserId(UUID.randomUUID()));
securityUser.setEmail("tenant@thingsboard.org");
securityUser.setAuthority(Authority.TENANT_ADMIN);
securityUser.setTenantId(new TenantId(UUID.randomUUID()));
securityUser.setEnabled(true);
securityUser.setFirstName("A");
securityUser.setLastName("B");
securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail()));
securityUser.setCustomerId(new CustomerId(UUID.randomUUID()));
testCreateAndParseAccessJwtToken(securityUser);
securityUser = new SecurityUser(securityUser, true, new UserPrincipal(UserPrincipal.Type.PUBLIC_ID, securityUser.getEmail()));
securityUser.setFirstName(null);
securityUser.setLastName(null);
securityUser.setCustomerId(null);
testCreateAndParseAccessJwtToken(securityUser);
}
public void testCreateAndParseAccessJwtToken(SecurityUser securityUser) {
AccessJwtToken accessToken = tokenFactory.createAccessJwtToken(securityUser);
checkExpirationTime(accessToken, jwtSettings.getTokenExpirationTime());
SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(new RawAccessJwtToken(accessToken.getToken()));
assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId());
assertThat(parsedSecurityUser.getEmail()).isEqualTo(securityUser.getEmail());
assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> {
return userPrincipal.getType().equals(securityUser.getUserPrincipal().getType())
&& userPrincipal.getValue().equals(securityUser.getUserPrincipal().getValue());
});
assertThat(parsedSecurityUser.getAuthorities()).isEqualTo(securityUser.getAuthorities());
assertThat(parsedSecurityUser.isEnabled()).isEqualTo(securityUser.isEnabled());
assertThat(parsedSecurityUser.getTenantId()).isEqualTo(securityUser.getTenantId());
assertThat(parsedSecurityUser.getCustomerId()).isEqualTo(securityUser.getCustomerId());
assertThat(parsedSecurityUser.getFirstName()).isEqualTo(securityUser.getFirstName());
assertThat(parsedSecurityUser.getLastName()).isEqualTo(securityUser.getLastName());
}
@Test
public void testCreateAndParseRefreshJwtToken() {
SecurityUser securityUser = new SecurityUser();
securityUser.setId(new UserId(UUID.randomUUID()));
securityUser.setEmail("tenant@thingsboard.org");
securityUser.setAuthority(Authority.TENANT_ADMIN);
securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail()));
securityUser.setEnabled(true);
securityUser.setTenantId(new TenantId(UUID.randomUUID()));
securityUser.setCustomerId(new CustomerId(UUID.randomUUID()));
JwtToken refreshToken = tokenFactory.createRefreshToken(securityUser);
checkExpirationTime(refreshToken, jwtSettings.getRefreshTokenExpTime());
SecurityUser parsedSecurityUser = tokenFactory.parseRefreshToken(new RawAccessJwtToken(refreshToken.getToken()));
assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId());
assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> {
return userPrincipal.getType().equals(securityUser.getUserPrincipal().getType())
&& userPrincipal.getValue().equals(securityUser.getUserPrincipal().getValue());
});
assertThat(parsedSecurityUser.getAuthority()).isNull();
}
@Test
public void testCreateAndParsePreVerificationJwtToken() {
SecurityUser securityUser = new SecurityUser();
securityUser.setId(new UserId(UUID.randomUUID()));
securityUser.setEmail("tenant@thingsboard.org");
securityUser.setAuthority(Authority.TENANT_ADMIN);
securityUser.setUserPrincipal(new UserPrincipal(UserPrincipal.Type.USER_NAME, securityUser.getEmail()));
securityUser.setEnabled(true);
securityUser.setTenantId(new TenantId(UUID.randomUUID()));
securityUser.setCustomerId(new CustomerId(UUID.randomUUID()));
int tokenLifetime = (int) TimeUnit.MINUTES.toSeconds(30);
JwtToken preVerificationToken = tokenFactory.createPreVerificationToken(securityUser, tokenLifetime);
checkExpirationTime(preVerificationToken, tokenLifetime);
SecurityUser parsedSecurityUser = tokenFactory.parseAccessJwtToken(new RawAccessJwtToken(preVerificationToken.getToken()));
assertThat(parsedSecurityUser.getId()).isEqualTo(securityUser.getId());
assertThat(parsedSecurityUser.getAuthority()).isEqualTo(Authority.PRE_VERIFICATION_TOKEN);
assertThat(parsedSecurityUser.getTenantId()).isEqualTo(securityUser.getTenantId());
assertThat(parsedSecurityUser.getCustomerId()).isEqualTo(securityUser.getCustomerId());
assertThat(parsedSecurityUser.getUserPrincipal()).matches(userPrincipal -> {
return userPrincipal.getType() == UserPrincipal.Type.USER_NAME
&& userPrincipal.getValue().equals(securityUser.getUserPrincipal().getValue());
});
}
private void checkExpirationTime(JwtToken jwtToken, int tokenLifetime) {
Claims claims = tokenFactory.parseTokenClaims(jwtToken).getBody();
assertThat(claims.getExpiration()).matches(actualExpirationTime -> {
Calendar expirationTime = Calendar.getInstance();
expirationTime.setTime(new Date());
expirationTime.add(Calendar.SECOND, tokenLifetime);
if (actualExpirationTime.equals(expirationTime.getTime())) {
return true;
} else if (actualExpirationTime.before(expirationTime.getTime())) {
int gap = 2;
expirationTime.add(Calendar.SECOND, -gap);
return actualExpirationTime.after(expirationTime.getTime());
} else {
return false;
}
});
}
}

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

@ -51,21 +51,24 @@ public interface UserService {
UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials);
void deleteUser(TenantId tenantId, UserId userId);
void deleteUser(TenantId tenantId, UserId userId);
PageData<User> findUsersByTenantId(TenantId tenantId, PageLink pageLink);
PageData<User> findTenantAdmins(TenantId tenantId, PageLink pageLink);
void deleteTenantAdmins(TenantId tenantId);
void deleteTenantAdmins(TenantId tenantId);
PageData<User> findCustomerUsers(TenantId tenantId, CustomerId customerId, PageLink pageLink);
void deleteCustomerUsers(TenantId tenantId, CustomerId customerId);
void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled);
void deleteCustomerUsers(TenantId tenantId, CustomerId customerId);
void setUserCredentialsEnabled(TenantId tenantId, UserId userId, boolean enabled);
void resetFailedLoginAttempts(TenantId tenantId, UserId userId);
int increaseFailedLoginAttempts(TenantId tenantId, UserId userId);
void onUserLoginSuccessful(TenantId tenantId, UserId userId);
void setLastLoginTs(TenantId tenantId, UserId userId);
int onUserLoginIncorrectCredentials(TenantId tenantId, UserId userId);
}

1
common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java

@ -33,4 +33,5 @@ public class CacheConstants {
public static final String OTA_PACKAGE_DATA_CACHE = "otaPackagesData";
public static final String REPOSITORY_SETTINGS_CACHE = "repositorySettings";
public static final String AUTO_COMMIT_SETTINGS_CACHE = "autoCommitSettings";
public static final String TWO_FA_VERIFICATION_CODES_CACHE = "twoFaVerificationCodes";
}

18
common/data/src/main/java/org/thingsboard/server/common/data/StringUtils.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.common.data;
import static org.apache.commons.lang3.StringUtils.repeat;
public class StringUtils {
public static final String EMPTY = "";
@ -74,4 +76,20 @@ public class StringUtils {
}
return null;
}
public static String obfuscate(String input, int seenMargin, char obfuscationChar,
int startIndexInclusive, int endIndexExclusive) {
String part = input.substring(startIndexInclusive, endIndexExclusive);
String obfuscatedPart;
if (part.length() <= seenMargin * 2) {
obfuscatedPart = repeat(obfuscationChar, part.length());
} else {
obfuscatedPart = part.substring(0, seenMargin)
+ repeat(obfuscationChar, part.length() - seenMargin * 2)
+ part.substring(part.length() - seenMargin);
}
return input.substring(0, startIndexInclusive) + obfuscatedPart + input.substring(endIndexExclusive);
}
}

2
common/data/src/main/java/org/thingsboard/server/common/data/TenantProfile.java

@ -21,6 +21,7 @@ import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.thingsboard.server.common.data.id.TenantProfileId;
import org.thingsboard.server.common.data.tenant.profile.DefaultTenantProfileConfiguration;
@ -36,6 +37,7 @@ import static org.thingsboard.server.common.data.SearchTextBasedWithAdditionalIn
@ApiModel
@Data
@ToString(exclude = {"profileDataBytes"})
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class TenantProfile extends SearchTextBased<TenantProfileId> implements HasName {

26
common/data/src/main/java/org/thingsboard/server/common/data/id/UserAuthSettingsId.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2022 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.id;
import java.util.UUID;
public class UserAuthSettingsId extends UUIDBased {
public UserAuthSettingsId(UUID id) {
super(id);
}
}

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

@ -16,11 +16,12 @@
package org.thingsboard.server.common.data.security;
public enum Authority {
SYS_ADMIN(0),
TENANT_ADMIN(1),
CUSTOMER_USER(2),
REFRESH_TOKEN(10);
REFRESH_TOKEN(10),
PRE_VERIFICATION_TOKEN(11);
private int code;

34
common/data/src/main/java/org/thingsboard/server/common/data/security/UserAuthSettings.java

@ -0,0 +1,34 @@
/**
* Copyright © 2016-2022 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.security;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.BaseData;
import org.thingsboard.server.common.data.id.UserAuthSettingsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
@Data
@EqualsAndHashCode(callSuper = true)
public class UserAuthSettings extends BaseData<UserAuthSettingsId> {
private static final long serialVersionUID = 2628320657987010348L;
private UserId userId;
private AccountTwoFaSettings twoFaSettings;
}

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

@ -0,0 +1,55 @@
/**
* Copyright © 2016-2022 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.security.model.mfa;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderConfig;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import java.util.List;
import java.util.Optional;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class PlatformTwoFaSettings {
@Valid
@NotNull
private List<TwoFaProviderConfig> providers;
@Min(value = 5, message = "minimum verification code sent period must be greater than or equal 5")
private Integer minVerificationCodeSendPeriod;
@Pattern(regexp = "[1-9]\\d*:[1-9]\\d*", message = "verification code check rate limit configuration is invalid")
private String verificationCodeCheckRateLimit;
@Min(value = 0, message = "maximum number of verification failure before user lockout must be positive")
private Integer maxVerificationFailuresBeforeUserLockout;
@Min(value = 60, message = "total amount of time allotted for verification must be greater than or equal 60")
private Integer totalAllowedTimeForVerification;
public Optional<TwoFaProviderConfig> getProviderConfig(TwoFaProviderType providerType) {
return Optional.ofNullable(providers)
.flatMap(providersConfigs -> providersConfigs.stream()
.filter(providerConfig -> providerConfig.getProviderType() == providerType)
.findFirst());
}
}

26
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/AccountTwoFaSettings.java

@ -0,0 +1,26 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.account;
import lombok.Data;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import java.util.LinkedHashMap;
@Data
public class AccountTwoFaSettings {
private LinkedHashMap<TwoFaProviderType, TwoFaAccountConfig> configs;
}

57
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/BackupCodeTwoFaAccountConfig.java

@ -0,0 +1,57 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.account;
import com.fasterxml.jackson.annotation.JsonGetter;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import javax.validation.constraints.NotEmpty;
import java.util.Set;
@Data
@EqualsAndHashCode(callSuper = true)
public class BackupCodeTwoFaAccountConfig extends TwoFaAccountConfig {
@NotEmpty
private Set<String> codes;
@Override
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.BACKUP_CODE;
}
@JsonGetter("codes")
private Set<String> getCodesForJson() {
if (serializeHiddenFields) {
return codes;
} else {
return null;
}
}
@JsonGetter
private Integer getCodesLeft() {
if (codes != null) {
return codes.size();
} else {
return null;
}
}
}

38
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/EmailTwoFaAccountConfig.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.account;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@Data
@EqualsAndHashCode(callSuper = true)
public class EmailTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig {
@NotBlank
@Email
private String email;
@Override
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.EMAIL;
}
}

24
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/OtpBasedTwoFaAccountConfig.java

@ -0,0 +1,24 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.account;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public abstract class OtpBasedTwoFaAccountConfig extends TwoFaAccountConfig {
}

38
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/SmsTwoFaAccountConfig.java

@ -0,0 +1,38 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.account;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@EqualsAndHashCode(callSuper = true)
@Data
public class SmsTwoFaAccountConfig extends OtpBasedTwoFaAccountConfig {
@NotBlank(message = "phone number cannot be blank")
@Pattern(regexp = "^\\+[1-9]\\d{1,14}$", message = "phone number is not of E.164 format")
private String phoneNumber;
@Override
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.SMS;
}
}

39
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TotpTwoFaAccountConfig.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.account;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@Data
@EqualsAndHashCode(callSuper = true)
public class TotpTwoFaAccountConfig extends TwoFaAccountConfig {
@NotBlank(message = "OTP auth URL cannot be blank")
@Pattern(regexp = "otpauth://totp/(\\S+?):(\\S+?)\\?issuer=(\\S+?)&secret=(\\w+?)", message = "OTP auth url is invalid")
private String authUrl;
@Override
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.TOTP;
}
}

47
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/account/TwoFaAccountConfig.java

@ -0,0 +1,47 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.account;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import lombok.Data;
import org.thingsboard.server.common.data.security.model.mfa.provider.TwoFaProviderType;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "providerType")
@JsonSubTypes({
@Type(name = "TOTP", value = TotpTwoFaAccountConfig.class),
@Type(name = "SMS", value = SmsTwoFaAccountConfig.class),
@Type(name = "EMAIL", value = EmailTwoFaAccountConfig.class),
@Type(name = "BACKUP_CODE", value = BackupCodeTwoFaAccountConfig.class)
})
@Data
public abstract class TwoFaAccountConfig {
private boolean useByDefault;
@JsonIgnore
protected transient boolean serializeHiddenFields;
@JsonIgnore
public abstract TwoFaProviderType getProviderType();
}

33
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/BackupCodeTwoFaProviderConfig.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.provider;
import lombok.Data;
import javax.validation.constraints.Min;
@Data
public class BackupCodeTwoFaProviderConfig implements TwoFaProviderConfig {
@Min(value = 1, message = "backup codes quantity must be greater than 0")
private int codesQuantity;
@Override
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.BACKUP_CODE;
}
}

30
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/EmailTwoFaProviderConfig.java

@ -0,0 +1,30 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.provider;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = true)
public class EmailTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig {
@Override
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.EMAIL;
}
}

28
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/OtpBasedTwoFaProviderConfig.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.provider;
import lombok.Data;
import javax.validation.constraints.Min;
@Data
public abstract class OtpBasedTwoFaProviderConfig implements TwoFaProviderConfig {
@Min(value = 1, message = "verification code lifetime is required")
private int verificationCodeLifetime;
}

37
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/SmsTwoFaProviderConfig.java

@ -0,0 +1,37 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.provider;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
@EqualsAndHashCode(callSuper = true)
@Data
public class SmsTwoFaProviderConfig extends OtpBasedTwoFaProviderConfig {
@NotBlank(message = "verification message template is required")
@Pattern(regexp = ".*\\$\\{code}.*", message = "template must contain verification code")
private String smsVerificationMessageTemplate;
@Override
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.SMS;
}
}

33
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TotpTwoFaProviderConfig.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.provider;
import lombok.Data;
import javax.validation.constraints.NotBlank;
@Data
public class TotpTwoFaProviderConfig implements TwoFaProviderConfig {
@NotBlank(message = "issuer name must not be blank")
private String issuerName;
@Override
public TwoFaProviderType getProviderType() {
return TwoFaProviderType.TOTP;
}
}

39
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderConfig.java

@ -0,0 +1,39 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.provider;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonSubTypes.Type;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
property = "providerType")
@JsonSubTypes({
@Type(name = "TOTP", value = TotpTwoFaProviderConfig.class),
@Type(name = "SMS", value = SmsTwoFaProviderConfig.class),
@Type(name = "EMAIL", value = EmailTwoFaProviderConfig.class),
@Type(name = "BACKUP_CODE", value = BackupCodeTwoFaProviderConfig.class)
})
public interface TwoFaProviderConfig {
@JsonIgnore
TwoFaProviderType getProviderType();
}

23
common/data/src/main/java/org/thingsboard/server/common/data/security/model/mfa/provider/TwoFaProviderType.java

@ -0,0 +1,23 @@
/**
* Copyright © 2016-2022 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.security.model.mfa.provider;
public enum TwoFaProviderType {
TOTP,
SMS,
EMAIL,
BACKUP_CODE
}

8
common/message/src/main/java/org/thingsboard/server/common/msg/tools/TbRateLimits.java

@ -17,6 +17,7 @@ package org.thingsboard.server.common.msg.tools;
import io.github.bucket4j.Bandwidth;
import io.github.bucket4j.Bucket4j;
import io.github.bucket4j.Refill;
import io.github.bucket4j.local.LocalBucket;
import io.github.bucket4j.local.LocalBucketBuilder;
@ -30,12 +31,17 @@ public class TbRateLimits {
private final String configuration;
public TbRateLimits(String limitsConfiguration) {
this(limitsConfiguration, false);
}
public TbRateLimits(String limitsConfiguration, boolean refillIntervally) {
LocalBucketBuilder builder = Bucket4j.builder();
boolean initialized = false;
for (String limitSrc : limitsConfiguration.split(",")) {
long capacity = Long.parseLong(limitSrc.split(":")[0]);
long duration = Long.parseLong(limitSrc.split(":")[1]);
builder.addLimit(Bandwidth.simple(capacity, Duration.ofSeconds(duration)));
Refill refill = refillIntervally ? Refill.intervally(capacity, Duration.ofSeconds(duration)) : Refill.greedy(capacity, Duration.ofSeconds(duration));
builder.addLimit(Bandwidth.classic(capacity, refill));
initialized = true;
}
if (initialized) {

83
common/message/src/test/java/org/thingsboard/server/common/msg/tools/RateLimitsTest.java

@ -0,0 +1,83 @@
/**
* Copyright © 2016-2022 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.msg.tools;
import org.junit.Test;
import java.util.concurrent.TimeUnit;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
public class RateLimitsTest {
@Test
public void testRateLimits_greedyRefill() {
testRateLimitWithGreedyRefill(3, 10);
testRateLimitWithGreedyRefill(3, 3);
testRateLimitWithGreedyRefill(4, 2);
}
private void testRateLimitWithGreedyRefill(int capacity, int period) {
String rateLimitConfig = capacity + ":" + period;
TbRateLimits rateLimits = new TbRateLimits(rateLimitConfig);
rateLimits.tryConsume(capacity);
assertThat(rateLimits.tryConsume()).as("new token is available").isFalse();
int expectedRefillTime = (int) (((double) period / capacity) * 1000);
int gap = 100;
for (int i = 0; i < capacity; i++) {
await("token refill for rate limit " + rateLimitConfig)
.atLeast(expectedRefillTime - gap, TimeUnit.MILLISECONDS)
.atMost(expectedRefillTime + gap, TimeUnit.MILLISECONDS)
.untilAsserted(() -> {
assertThat(rateLimits.tryConsume()).as("token is available").isTrue();
});
assertThat(rateLimits.tryConsume()).as("new token is available").isFalse();
}
}
@Test
public void testRateLimits_intervalRefill() {
testRateLimitWithIntervalRefill(10, 5);
testRateLimitWithIntervalRefill(3, 3);
testRateLimitWithIntervalRefill(4, 2);
}
private void testRateLimitWithIntervalRefill(int capacity, int period) {
String rateLimitConfig = capacity + ":" + period;
TbRateLimits rateLimits = new TbRateLimits(rateLimitConfig, true);
rateLimits.tryConsume(capacity);
assertThat(rateLimits.tryConsume()).as("new token is available").isFalse();
int expectedRefillTime = period * 1000;
int gap = 300;
await("tokens refill for rate limit " + rateLimitConfig)
.atLeast(expectedRefillTime - gap, TimeUnit.MILLISECONDS)
.atMost(expectedRefillTime + gap, TimeUnit.MILLISECONDS)
.untilAsserted(() -> {
for (int i = 0; i < capacity; i++) {
assertThat(rateLimits.tryConsume()).as("token is available").isTrue();
}
assertThat(rateLimits.tryConsume()).as("new token is available").isFalse();
});
}
}

2
common/queue/src/main/java/org/thingsboard/server/queue/discovery/HashPartitionService.java

@ -222,8 +222,6 @@ public class HashPartitionService implements PartitionService {
@Override
public synchronized void recalculatePartitions(ServiceInfo currentService, List<ServiceInfo> otherServices) {
partitionsInit();
tbTransportServicesByType.clear();
logServiceInfo(currentService);
otherServices.forEach(this::logServiceInfo);

5
common/util/src/main/java/org/thingsboard/common/util/CollectionsUtil.java

@ -34,4 +34,9 @@ public class CollectionsUtil {
public static <T> Set<T> diffSets(Set<T> a, Set<T> b) {
return b.stream().filter(p -> !a.contains(p)).collect(Collectors.toSet());
}
public static <T> boolean contains(Collection<T> collection, T element) {
return isNotEmpty(collection) && collection.contains(element);
}
}

7
dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelFilter.java

@ -15,6 +15,8 @@
*/
package org.thingsboard.server.dao.audit;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.EntityType;
import org.thingsboard.server.common.data.audit.ActionType;
@ -22,11 +24,14 @@ import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
@Component
@ConditionalOnProperty(prefix = "audit-log", value = "enabled", havingValue = "true")
public class AuditLogLevelFilter {
private Map<EntityType, AuditLogLevelMask> entityTypeMask = new HashMap<>();
public AuditLogLevelFilter(Map<String, String> mask) {
public AuditLogLevelFilter(AuditLogLevelProperties auditLogLevelProperties) {
Map<String, String> mask = auditLogLevelProperties.getMask();
entityTypeMask.clear();
mask.forEach((entityTypeStr, logLevelMaskStr) -> {
EntityType entityType = EntityType.valueOf(entityTypeStr.toUpperCase(Locale.ENGLISH));

2
application/src/main/java/org/thingsboard/server/config/AuditLogLevelProperties.java → dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogLevelProperties.java

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.thingsboard.server.config;
package org.thingsboard.server.dao.audit;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

1
dao/src/main/java/org/thingsboard/server/dao/audit/AuditLogServiceImpl.java

@ -25,6 +25,7 @@ import com.google.common.util.concurrent.ListenableFuture;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.thingsboard.common.util.JacksonUtil;

7
dao/src/main/java/org/thingsboard/server/dao/model/ModelConstants.java

@ -563,6 +563,13 @@ public class ModelConstants {
public static final String EXTERNAL_ID_PROPERTY = "external_id";
/**
* User auth settings constants.
* */
public static final String USER_AUTH_SETTINGS_COLUMN_FAMILY_NAME = "user_auth_settings";
public static final String USER_AUTH_SETTINGS_USER_ID_PROPERTY = USER_ID_PROPERTY;
public static final String USER_AUTH_SETTINGS_TWO_FA_SETTINGS = "two_fa_settings";
/**
* Cassandra attributes and timeseries constants.
*/

81
dao/src/main/java/org/thingsboard/server/dao/model/sql/UserAuthSettingsEntity.java

@ -0,0 +1,81 @@
/**
* Copyright © 2016-2022 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.dao.model.sql;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.thingsboard.common.util.JacksonUtil;
import org.thingsboard.server.common.data.id.UserAuthSettingsId;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserAuthSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.AccountTwoFaSettings;
import org.thingsboard.server.common.data.security.model.mfa.account.TwoFaAccountConfig;
import org.thingsboard.server.dao.model.BaseEntity;
import org.thingsboard.server.dao.model.BaseSqlEntity;
import org.thingsboard.server.dao.model.ModelConstants;
import org.thingsboard.server.dao.util.mapping.JsonStringType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;
import java.util.UUID;
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@TypeDef(name = "json", typeClass = JsonStringType.class)
@Entity
@Table(name = ModelConstants.USER_AUTH_SETTINGS_COLUMN_FAMILY_NAME)
public class UserAuthSettingsEntity extends BaseSqlEntity<UserAuthSettings> implements BaseEntity<UserAuthSettings> {
@Column(name = ModelConstants.USER_AUTH_SETTINGS_USER_ID_PROPERTY, nullable = false, unique = true)
private UUID userId;
@Type(type = "json")
@Column(name = ModelConstants.USER_AUTH_SETTINGS_TWO_FA_SETTINGS)
private JsonNode twoFaSettings;
public UserAuthSettingsEntity(UserAuthSettings userAuthSettings) {
if (userAuthSettings.getId() != null) {
this.setId(userAuthSettings.getId().getId());
}
this.setCreatedTime(userAuthSettings.getCreatedTime());
if (userAuthSettings.getUserId() != null) {
this.userId = userAuthSettings.getUserId().getId();
}
if (userAuthSettings.getTwoFaSettings() != null) {
this.twoFaSettings = JacksonUtil.valueToTree(userAuthSettings.getTwoFaSettings());
}
}
@Override
public UserAuthSettings toData() {
UserAuthSettings userAuthSettings = new UserAuthSettings();
userAuthSettings.setId(new UserAuthSettingsId(id));
userAuthSettings.setCreatedTime(createdTime);
if (userId != null) {
userAuthSettings.setUserId(new UserId(userId));
}
if (twoFaSettings != null) {
userAuthSettings.setTwoFaSettings(JacksonUtil.treeToValue(twoFaSettings, AccountTwoFaSettings.class));
}
return userAuthSettings;
}
}

4
dao/src/main/java/org/thingsboard/server/dao/service/ConstraintValidator.java

@ -21,10 +21,10 @@ import org.hibernate.validator.HibernateValidatorConfiguration;
import org.hibernate.validator.cfg.ConstraintMapping;
import org.thingsboard.server.common.data.validation.Length;
import org.thingsboard.server.common.data.validation.NoXss;
import org.thingsboard.server.dao.exception.DataValidationException;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.ValidationException;
import javax.validation.Validator;
import java.util.List;
import java.util.Set;
@ -46,7 +46,7 @@ public class ConstraintValidator {
.distinct()
.collect(Collectors.toList());
if (!validationErrors.isEmpty()) {
throw new ValidationException("Validation error: " + String.join(", ", validationErrors));
throw new DataValidationException("Validation error: " + String.join(", ", validationErrors));
}
}

56
dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserAuthSettingsDao.java

@ -0,0 +1,56 @@
/**
* Copyright © 2016-2022 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.dao.sql.user;
import lombok.RequiredArgsConstructor;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Component;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserAuthSettings;
import org.thingsboard.server.dao.DaoUtil;
import org.thingsboard.server.dao.model.sql.UserAuthSettingsEntity;
import org.thingsboard.server.dao.sql.JpaAbstractDao;
import org.thingsboard.server.dao.user.UserAuthSettingsDao;
import java.util.UUID;
@Component
@RequiredArgsConstructor
public class JpaUserAuthSettingsDao extends JpaAbstractDao<UserAuthSettingsEntity, UserAuthSettings> implements UserAuthSettingsDao {
private final UserAuthSettingsRepository repository;
@Override
public UserAuthSettings findByUserId(UserId userId) {
return DaoUtil.getData(repository.findByUserId(userId.getId()));
}
@Override
public void removeByUserId(UserId userId) {
repository.deleteByUserId(userId.getId());
}
@Override
protected Class<UserAuthSettingsEntity> getEntityClass() {
return UserAuthSettingsEntity.class;
}
@Override
protected JpaRepository<UserAuthSettingsEntity, UUID> getRepository() {
return repository;
}
}

33
dao/src/main/java/org/thingsboard/server/dao/sql/user/UserAuthSettingsRepository.java

@ -0,0 +1,33 @@
/**
* Copyright © 2016-2022 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.dao.sql.user;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import org.thingsboard.server.dao.model.sql.UserAuthSettingsEntity;
import java.util.UUID;
@Repository
public interface UserAuthSettingsRepository extends JpaRepository<UserAuthSettingsEntity, UUID> {
UserAuthSettingsEntity findByUserId(UUID userId);
@Transactional
void deleteByUserId(UUID userId);
}

28
dao/src/main/java/org/thingsboard/server/dao/user/UserAuthSettingsDao.java

@ -0,0 +1,28 @@
/**
* Copyright © 2016-2022 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.dao.user;
import org.thingsboard.server.common.data.id.UserId;
import org.thingsboard.server.common.data.security.UserAuthSettings;
import org.thingsboard.server.dao.Dao;
public interface UserAuthSettingsDao extends Dao<UserAuthSettings> {
UserAuthSettings findByUserId(UserId userId);
void removeByUserId(UserId userId);
}

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

@ -68,17 +68,20 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
private final UserDao userDao;
private final UserCredentialsDao userCredentialsDao;
private final UserAuthSettingsDao userAuthSettingsDao;
private final DataValidator<User> userValidator;
private final DataValidator<UserCredentials> userCredentialsValidator;
private final ApplicationEventPublisher eventPublisher;
public UserServiceImpl(UserDao userDao,
UserCredentialsDao userCredentialsDao,
UserAuthSettingsDao userAuthSettingsDao,
DataValidator<User> userValidator,
DataValidator<UserCredentials> userCredentialsValidator,
ApplicationEventPublisher eventPublisher) {
this.userDao = userDao;
this.userCredentialsDao = userCredentialsDao;
this.userAuthSettingsDao = userAuthSettingsDao;
this.userValidator = userValidator;
this.userCredentialsValidator = userCredentialsValidator;
this.eventPublisher = eventPublisher;
@ -215,6 +218,7 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
validateId(userId, INCORRECT_USER_ID + userId);
UserCredentials userCredentials = userCredentialsDao.findByUserId(tenantId, userId.getId());
userCredentialsDao.removeById(tenantId, userCredentials.getUuidId());
userAuthSettingsDao.removeByUserId(userId);
deleteEntityRelations(tenantId, userId);
userDao.removeById(tenantId, userId.getId());
eventPublisher.publishEvent(new UserAuthDataChangedEvent(userId));
@ -283,34 +287,36 @@ public class UserServiceImpl extends AbstractEntityService implements UserServic
@Override
public void onUserLoginSuccessful(TenantId tenantId, UserId userId) {
public void resetFailedLoginAttempts(TenantId tenantId, UserId userId) {
log.trace("Executing onUserLoginSuccessful [{}]", userId);
User user = findUserById(tenantId, userId);
setLastLoginTs(user);
resetFailedLoginAttempts(user);
saveUser(user);
}
private void setLastLoginTs(User user) {
private void resetFailedLoginAttempts(User user) {
JsonNode additionalInfo = user.getAdditionalInfo();
if (!(additionalInfo instanceof ObjectNode)) {
additionalInfo = JacksonUtil.newObjectNode();
}
((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis());
((ObjectNode) additionalInfo).put(FAILED_LOGIN_ATTEMPTS, 0);
user.setAdditionalInfo(additionalInfo);
}
private void resetFailedLoginAttempts(User user) {
@Override
public void setLastLoginTs(TenantId tenantId, UserId userId) {
User user = findUserById(tenantId, userId);
JsonNode additionalInfo = user.getAdditionalInfo();
if (!(additionalInfo instanceof ObjectNode)) {
additionalInfo = JacksonUtil.newObjectNode();
}
((ObjectNode) additionalInfo).put(FAILED_LOGIN_ATTEMPTS, 0);
((ObjectNode) additionalInfo).put(LAST_LOGIN_TS, System.currentTimeMillis());
user.setAdditionalInfo(additionalInfo);
saveUser(user);
}
@Override
public int onUserLoginIncorrectCredentials(TenantId tenantId, UserId userId) {
public int increaseFailedLoginAttempts(TenantId tenantId, UserId userId) {
log.trace("Executing onUserLoginIncorrectCredentials [{}]", userId);
User user = findUserById(tenantId, userId);
int failedLoginAttempts = increaseFailedLoginAttempts(user);

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save