From b5afb32f569c88ec56a77a9359578d2f0bd36a0d Mon Sep 17 00:00:00 2001 From: Viacheslav Klimov Date: Fri, 18 Mar 2022 11:05:17 +0200 Subject: [PATCH] 2FA: validation and error handling refactoring --- .../server/controller/BaseController.java | 15 ++++++ .../controller/TwoFactorAuthController.java | 47 ++++++++++++++----- .../auth/mfa/TwoFactorAuthService.java | 23 +++++---- .../TotpTwoFactorAuthAccountConfig.java | 2 + .../SmsTwoFactorAuthProviderConfig.java | 2 +- .../TotpTwoFactorAuthProviderConfig.java | 2 +- .../mfa/provider/TwoFactorAuthProvider.java | 3 +- .../impl/SmsTwoFactorAuthProvider.java | 4 +- .../auth/rest/RestAuthenticationProvider.java | 1 + ...RestAwareAuthenticationSuccessHandler.java | 41 +++++++++------- .../common/util/ThrowingBiConsumer.java | 21 +++++++++ ...eFunction.java => ThrowingBiFunction.java} | 4 +- .../common/util/ThrowingTripleConsumer.java | 21 +++++++++ .../common/util/ThrowingTripleFunction.java | 21 +++++++++ 14 files changed, 163 insertions(+), 44 deletions(-) create mode 100644 common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java rename common/util/src/main/java/org/thingsboard/common/util/{TripleFunction.java => ThrowingBiFunction.java} (88%) create mode 100644 common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java create mode 100644 common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java diff --git a/application/src/main/java/org/thingsboard/server/controller/BaseController.java b/application/src/main/java/org/thingsboard/server/controller/BaseController.java index 6dee3b6114..fdef4cbc0c 100644 --- a/application/src/main/java/org/thingsboard/server/controller/BaseController.java +++ b/application/src/main/java/org/thingsboard/server/controller/BaseController.java @@ -23,9 +23,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.thingsboard.server.cluster.TbClusterService; import org.thingsboard.server.common.data.Customer; @@ -145,6 +147,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; @@ -334,6 +337,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 checkNotNull(T reference) throws ThingsboardException { return checkNotNull(reference, "Requested item wasn't found!"); } diff --git a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java index f0d50c2600..b6ad9626b1 100644 --- a/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java +++ b/application/src/main/java/org/thingsboard/server/controller/TwoFactorAuthController.java @@ -30,7 +30,7 @@ import org.springframework.web.bind.annotation.RestController; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.exception.ThingsboardErrorCode; import org.thingsboard.server.common.data.exception.ThingsboardException; -import org.thingsboard.server.dao.service.ConstraintValidator; +import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.auth.mfa.TwoFactorAuthService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; import org.thingsboard.server.service.security.auth.mfa.config.account.TotpTwoFactorAuthAccountConfig; @@ -42,10 +42,25 @@ import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; +import javax.validation.Valid; -// FIXME: Swagger documentation -// FIXME: tests for 2FA - +/* + * + * TODO [viacheslav]: + * - 2FA should be mandatory when logging in and must be rolled out to all existing users when 2FA is activated. + * - Rate limits should be implemented to protect against brute force leaked accounts to prevent SMS cost explosion. + * - Configurable softlock after XX (3) attempts: XX (15) mins + * - Configurable hardlock (user blocking) after a total of XX (10) unsuccessful attempts. + * - The OTP token should only be valid for XX (5) minutes. + * - Disable 2FA only possible after successful 2FA auth - it is possible with simple password resest + * - 2FA entries should be secured against code injection by code validation. + * - Email 2FA provider + * + * FIXME [viacheslav]: + * - Tests for 2FA + * - Swagger documentation + * + * */ @RestController @RequestMapping("/api") @RequiredArgsConstructor @@ -65,7 +80,7 @@ public class TwoFactorAuthController extends BaseController { @PostMapping("/2fa/account/config/generate") @PreAuthorize("isAuthenticated()") - public TwoFactorAuthAccountConfig generateTwoFactorAuthAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws ThingsboardException { + public TwoFactorAuthAccountConfig generateTwoFactorAuthAccountConfig(@RequestParam TwoFactorAuthProviderType providerType) throws Exception { SecurityUser user = getCurrentUser(); return twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), providerType, @@ -90,20 +105,19 @@ public class TwoFactorAuthController extends BaseController { @PostMapping("/2fa/account/config/submit") @PreAuthorize("isAuthenticated()") - public void submitTwoFactorAuthAccountConfig(@RequestBody TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException { + public void submitTwoFactorAuthAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig) throws Exception { SecurityUser user = getCurrentUser(); twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(), (provider, providerConfig) -> { - ConstraintValidator.validateFields(accountConfig); provider.prepareVerificationCode(user, providerConfig, accountConfig); }); } @PostMapping("/2fa/account/config") @PreAuthorize("isAuthenticated()") - public void verifyAndSaveTwoFactorAuthAccountConfig(@RequestBody TwoFactorAuthAccountConfig accountConfig, - @RequestParam String verificationCode) throws ThingsboardException { + public void verifyAndSaveTwoFactorAuthAccountConfig(@Valid @RequestBody TwoFactorAuthAccountConfig accountConfig, + @RequestParam String verificationCode) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), accountConfig.getProviderType(), @@ -127,14 +141,14 @@ public class TwoFactorAuthController extends BaseController { @PostMapping("/2fa/settings") @PreAuthorize("hasAnyAuthority('SYS_ADMIN', 'TENANT_ADMIN')") - public void saveTwoFactorAuthSettings(@RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException { + public void saveTwoFactorAuthSettings(@Valid @RequestBody TwoFactorAuthSettings twoFactorAuthSettings) throws ThingsboardException { twoFactorAuthService.saveTwoFaSettings(getTenantId(), twoFactorAuthSettings); } @PostMapping("/auth/2fa/verification/check") @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") - public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode) throws ThingsboardException { + public JwtTokenPair checkTwoFaVerificationCode(@RequestParam String verificationCode) throws Exception { SecurityUser user = getCurrentUser(); boolean verificationSuccess = twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(), @@ -149,4 +163,15 @@ public class TwoFactorAuthController extends BaseController { } } + @PostMapping("/auth/2fa/verification/resend") + @PreAuthorize("hasAuthority('PRE_VERIFICATION_TOKEN')") + public void resendTwoFaVerificationCode() throws Exception { + SecurityUser user = getCurrentUser(); + + twoFactorAuthService.processByTwoFaProvider(user.getTenantId(), user.getId(), + (provider, providerConfig, accountConfig) -> { + provider.prepareVerificationCode(user, providerConfig, accountConfig); + }); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java index 2b44fddeb5..580a12ce44 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/TwoFactorAuthService.java @@ -21,7 +21,10 @@ import lombok.SneakyThrows; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.thingsboard.common.util.JacksonUtil; -import org.thingsboard.common.util.TripleFunction; +import org.thingsboard.common.util.ThrowingBiConsumer; +import org.thingsboard.common.util.ThrowingBiFunction; +import org.thingsboard.common.util.ThrowingTripleConsumer; +import org.thingsboard.common.util.ThrowingTripleFunction; import org.thingsboard.server.common.data.AdminSettings; import org.thingsboard.server.common.data.DataConstants; import org.thingsboard.server.common.data.User; @@ -32,7 +35,6 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.kv.BaseAttributeKvEntry; import org.thingsboard.server.common.data.kv.JsonDataEntry; import org.thingsboard.server.dao.attributes.AttributesService; -import org.thingsboard.server.dao.service.ConstraintValidator; import org.thingsboard.server.dao.settings.AdminSettingsService; import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.mfa.config.TwoFactorAuthSettings; @@ -47,8 +49,6 @@ import java.util.EnumMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; -import java.util.function.BiConsumer; -import java.util.function.BiFunction; @Service @RequiredArgsConstructor @@ -81,7 +81,7 @@ public class TwoFactorAuthService { } - public R processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, BiFunction, TwoFactorAuthProviderConfig, R> function) throws ThingsboardException { + public R processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, ThrowingBiFunction, TwoFactorAuthProviderConfig, R> function) throws Exception { TwoFactorAuthProviderConfig providerConfig = getTwoFaProviderConfig(tenantId, providerType) .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); TwoFactorAuthProvider provider = getTwoFaProvider(providerType) @@ -90,14 +90,14 @@ public class TwoFactorAuthService { return function.apply(provider, providerConfig); } - public void processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, BiConsumer, TwoFactorAuthProviderConfig> function) throws ThingsboardException { + public void processByTwoFaProvider(TenantId tenantId, TwoFactorAuthProviderType providerType, ThrowingBiConsumer, TwoFactorAuthProviderConfig> function) throws Exception { processByTwoFaProvider(tenantId, providerType, (provider, providerConfig) -> { function.accept(provider, providerConfig); return null; }); } - public R processByTwoFaProvider(TenantId tenantId, UserId userId, TripleFunction, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig, R> function) throws ThingsboardException { + public R processByTwoFaProvider(TenantId tenantId, UserId userId, ThrowingTripleFunction, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig, R> function) throws Exception { TwoFactorAuthAccountConfig accountConfig = getTwoFaAccountConfig(tenantId, userId) .orElseThrow(() -> new ThingsboardException("2FA is not configured for user", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); @@ -109,6 +109,13 @@ public class TwoFactorAuthService { return function.apply(provider, providerConfig, accountConfig); } + public void processByTwoFaProvider(TenantId tenantId, UserId userId, ThrowingTripleConsumer, TwoFactorAuthProviderConfig, TwoFactorAuthAccountConfig> function) throws Exception { + processByTwoFaProvider(tenantId, userId, (provider, providerConfig, accountConfig) -> { + function.accept(provider, providerConfig, accountConfig); + return null; + }); + } + public Optional getTwoFaAccountConfig(TenantId tenantId, UserId userId) { User user = userService.findUserById(tenantId, userId); @@ -121,7 +128,6 @@ public class TwoFactorAuthService { } public void saveTwoFaAccountConfig(TenantId tenantId, UserId userId, TwoFactorAuthAccountConfig accountConfig) throws ThingsboardException { - ConstraintValidator.validateFields(accountConfig); getTwoFaProviderConfig(tenantId, accountConfig.getProviderType()) .orElseThrow(() -> new ThingsboardException("2FA provider is not configured", ThingsboardErrorCode.BAD_REQUEST_PARAMS)); @@ -160,7 +166,6 @@ public class TwoFactorAuthService { @SneakyThrows({InterruptedException.class, ExecutionException.class}) public void saveTwoFaSettings(TenantId tenantId, TwoFactorAuthSettings twoFactorAuthSettings) { - ConstraintValidator.validateFields(twoFactorAuthSettings); if (tenantId.equals(TenantId.SYS_TENANT_ID)) { AdminSettings settings = Optional.ofNullable(adminSettingsService.findAdminSettingsByKey(tenantId, TWO_FACTOR_AUTH_SETTINGS_KEY)) .orElseGet(() -> { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java index a48cf162a0..78ab4cf80b 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/account/TotpTwoFactorAuthAccountConfig.java @@ -19,11 +19,13 @@ import lombok.Data; import org.thingsboard.server.service.security.auth.mfa.provider.TwoFactorAuthProviderType; import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; @Data public class TotpTwoFactorAuthAccountConfig implements TwoFactorAuthAccountConfig { @NotBlank +// @Pattern(regexp = ) // TODO [viacheslav]: validate otp auth url by pattern private String authUrl; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java index a036e47574..2d15a07ad1 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/SmsTwoFactorAuthProviderConfig.java @@ -25,7 +25,7 @@ import javax.validation.constraints.Pattern; public class SmsTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { @NotBlank - @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "Template must contain verification code") + @Pattern(regexp = ".*\\$\\{verificationCode}.*", message = "template must contain verification code") private String smsVerificationMessageTemplate; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java index 2d6cc5ddf5..e1604bb618 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/config/provider/TotpTwoFactorAuthProviderConfig.java @@ -23,7 +23,7 @@ import javax.validation.constraints.NotBlank; @Data public class TotpTwoFactorAuthProviderConfig implements TwoFactorAuthProviderConfig { - @NotBlank(message = "Issuer name must not be blank") + @NotBlank(message = "issuer name must not be blank") private String issuerName; @Override diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java index 82122a9163..011404268c 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/mfa/provider/TwoFactorAuthProvider.java @@ -16,6 +16,7 @@ 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.service.security.auth.mfa.config.account.TwoFactorAuthAccountConfig; import org.thingsboard.server.service.security.auth.mfa.config.provider.TwoFactorAuthProviderConfig; import org.thingsboard.server.service.security.model.SecurityUser; @@ -24,7 +25,7 @@ public interface TwoFactorAuthProvider twoFaAccountConfig = twoFactorAuthService.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()); - if (twoFaAccountConfig.isPresent()) { - try { - twoFactorAuthService.processByTwoFaProvider(securityUser.getTenantId(), twoFaAccountConfig.get().getProviderType(), - (provider, providerConfig) -> { - provider.prepareVerificationCode(securityUser, providerConfig, twoFaAccountConfig.get()); - }); - tokenPair = new JwtTokenPair(); - tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser).getToken()); - tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); - } catch (Exception e) { - log.error("Failed to process 2FA for user {}. Falling back to plain auth", securityUser.getId(), e); - tokenPair = tokenFactory.createTokenPair(securityUser); - } + if (authentication instanceof UsernamePasswordAuthenticationToken) { + // TODO [viacheslav]: or maybe create another AuthenticationProvider and put it after rest authentication provider ? + tokenPair = processTwoFa(securityUser); } else { tokenPair = tokenFactory.createTokenPair(securityUser); } @@ -81,6 +68,26 @@ public class RestAwareAuthenticationSuccessHandler implements AuthenticationSucc clearAuthenticationAttributes(request); } + private JwtTokenPair processTwoFa(SecurityUser securityUser) { + Optional twoFaAccountConfig = twoFactorAuthService.getTwoFaAccountConfig(securityUser.getTenantId(), securityUser.getId()); + if (twoFaAccountConfig.isPresent()) { + try { + twoFactorAuthService.processByTwoFaProvider(securityUser.getTenantId(), twoFaAccountConfig.get().getProviderType(), + (provider, providerConfig) -> { + provider.prepareVerificationCode(securityUser, providerConfig, twoFaAccountConfig.get()); + }); + JwtTokenPair tokenPair = new JwtTokenPair(); + tokenPair.setToken(tokenFactory.createPreVerificationToken(securityUser).getToken()); + tokenPair.setScope(Authority.PRE_VERIFICATION_TOKEN); + return tokenPair; + } catch (Exception e) { + // TODO [viacheslav]: write audit log + log.error("Failed to process 2FA for user {}. Falling back to plain auth", securityUser.getId(), e); + } + } + return tokenFactory.createTokenPair(securityUser); + } + /** * Removes temporary authentication-related data which may have been stored * in the session during the authentication process.. diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java new file mode 100644 index 0000000000..269f9f75cb --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiConsumer.java @@ -0,0 +1,21 @@ +/** + * 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.common.util; + +@FunctionalInterface +public interface ThrowingBiConsumer { + void accept(A a, B b) throws Exception; +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiFunction.java similarity index 88% rename from common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java rename to common/util/src/main/java/org/thingsboard/common/util/ThrowingBiFunction.java index 5134703cd3..32058df26e 100644 --- a/common/util/src/main/java/org/thingsboard/common/util/TripleFunction.java +++ b/common/util/src/main/java/org/thingsboard/common/util/ThrowingBiFunction.java @@ -16,6 +16,6 @@ package org.thingsboard.common.util; @FunctionalInterface -public interface TripleFunction { - R apply(A a, B b, C c); +public interface ThrowingBiFunction { + R apply(A a, B b) throws Exception; } diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java new file mode 100644 index 0000000000..5230da4863 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleConsumer.java @@ -0,0 +1,21 @@ +/** + * 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.common.util; + +@FunctionalInterface +public interface ThrowingTripleConsumer { + void accept(A a, B b, C c) throws Exception; +} diff --git a/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java b/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java new file mode 100644 index 0000000000..cade9d1717 --- /dev/null +++ b/common/util/src/main/java/org/thingsboard/common/util/ThrowingTripleFunction.java @@ -0,0 +1,21 @@ +/** + * 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.common.util; + +@FunctionalInterface +public interface ThrowingTripleFunction { + R apply(A a, B b, C c) throws Exception; +}