From 1f884bb5ca144279722b53a761b4ad9ea574f5c6 Mon Sep 17 00:00:00 2001 From: Andrii Landiak Date: Wed, 15 Oct 2025 12:23:03 +0300 Subject: [PATCH] UserAuthDetailsCache. Refactoring after review --- .../ThingsboardSecurityConfiguration.java | 20 +- .../server/controller/ApiKeyController.java | 31 +-- .../entitiy/EntityStateSourcingListener.java | 8 +- .../queue/DefaultTbClusterService.java | 3 +- .../AbstractHeaderTokenExtractor.java | 4 +- ...wtTokenAuthenticationProcessingFilter.java | 8 +- .../RefreshTokenAuthenticationProvider.java | 32 ++- .../pat/ApiKeyAuthenticationProvider.java | 34 ++- .../auth/pat/ApiKeyAuthenticationToken.java | 12 +- ...eyTokenAuthenticationProcessingFilter.java | 14 +- .../auth/rest/RestAuthenticationProvider.java | 3 +- .../{RawApiKeyToken.java => RawApiKey.java} | 4 +- .../service/ttl/ApiKeysCleanUpService.java | 8 +- .../cache/DefaultUserAuthDetailsCache.java | 85 +++++++ .../user/cache/UserAuthDetailsCache.java | 26 +++ .../src/main/resources/thingsboard.yml | 13 +- .../server/controller/AbstractWebTest.java | 55 ++++- .../controller/ApiKeyControllerTest.java | 4 +- .../security/auth/TokenOutdatingTest.java | 14 +- .../pat/ApiKeyAuthenticationProviderTest.java | 208 ++++++------------ .../server/dao/user/UserService.java | 3 + .../server/common/data/CacheConstants.java | 1 + .../server/common/data/UserAuthDetails.java | 18 ++ .../msg/plugin/ComponentLifecycleMsg.java | 3 - .../server/dao/model/sql/UserEntity.java | 3 - .../server/dao/pat/ApiKeyCacheKey.java | 40 ++++ .../server/dao/pat/ApiKeyCaffeineCache.java | 33 +++ .../thingsboard/server/dao/pat/ApiKeyDao.java | 6 +- .../server/dao/pat/ApiKeyEvictEvent.java | 18 ++ .../server/dao/pat/ApiKeyRedisCache.java | 36 +++ .../server/dao/pat/ApiKeyServiceImpl.java | 66 +++--- .../validator/ApiKeyDataValidator.java | 18 +- .../server/dao/sql/pat/ApiKeyRepository.java | 21 +- .../server/dao/sql/pat/JpaApiKeyDao.java | 8 +- .../server/dao/sql/user/JpaUserDao.java | 8 + .../server/dao/sql/user/UserRepository.java | 9 +- .../server/dao/tenant/TenantServiceImpl.java | 2 +- .../thingsboard/server/dao/user/UserDao.java | 3 + .../server/dao/user/UserServiceImpl.java | 10 +- .../main/resources/sql/schema-entities.sql | 4 +- .../thingsboard/rest/client/RestClient.java | 10 +- 41 files changed, 587 insertions(+), 319 deletions(-) rename application/src/main/java/org/thingsboard/server/service/security/model/token/{RawApiKeyToken.java => RawApiKey.java} (93%) create mode 100644 application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java create mode 100644 application/src/main/java/org/thingsboard/server/service/user/cache/UserAuthDetailsCache.java create mode 100644 common/data/src/main/java/org/thingsboard/server/common/data/UserAuthDetails.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCacheKey.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCaffeineCache.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyEvictEvent.java create mode 100644 dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyRedisCache.java diff --git a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java index d4e6bf2f8f..b467f01f35 100644 --- a/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java +++ b/application/src/main/java/org/thingsboard/server/config/ThingsboardSecurityConfiguration.java @@ -61,9 +61,9 @@ import org.thingsboard.server.service.security.auth.rest.RestLoginProcessingFilt import org.thingsboard.server.service.security.auth.rest.RestPublicLoginProcessingFilter; import org.thingsboard.server.transport.http.config.PayloadSizeFilter; -import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Stream; @Configuration @EnableWebSecurity @@ -72,8 +72,8 @@ import java.util.List; @TbCoreComponent public class ThingsboardSecurityConfiguration { - public static final String JWT_TOKEN_HEADER_PARAM = "X-Authorization"; - public static final String JWT_TOKEN_HEADER_PARAM_V2 = "Authorization"; + public static final String AUTHORIZATION_HEADER = "X-Authorization"; + public static final String AUTHORIZATION_HEADER_V2 = "Authorization"; public static final String JWT_TOKEN_QUERY_PARAM = "token"; public static final String API_KEY_HEADER_PREFIX = "ApiKey "; @@ -192,10 +192,16 @@ public class ThingsboardSecurityConfiguration { } private SkipPathRequestMatcher buildSkipPathRequestMatcher() { - List pathsToSkip = new ArrayList<>(Arrays.asList(NON_TOKEN_BASED_AUTH_ENTRY_POINTS)); - pathsToSkip.addAll(Arrays.asList(WS_ENTRY_POINT, TOKEN_REFRESH_ENTRY_POINT, FORM_BASED_LOGIN_ENTRY_POINT, - PUBLIC_LOGIN_ENTRY_POINT, DEVICE_API_ENTRY_POINT, MAIL_OAUTH2_PROCESSING_ENTRY_POINT, - DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT)); + List pathsToSkip = Stream.concat( + Arrays.stream(NON_TOKEN_BASED_AUTH_ENTRY_POINTS), + Stream.of( + WS_ENTRY_POINT, + TOKEN_REFRESH_ENTRY_POINT, + FORM_BASED_LOGIN_ENTRY_POINT, + PUBLIC_LOGIN_ENTRY_POINT, + DEVICE_API_ENTRY_POINT, + MAIL_OAUTH2_PROCESSING_ENTRY_POINT, + DEVICE_CONNECTIVITY_CERTIFICATE_DOWNLOAD_ENTRY_POINT)).toList(); return new SkipPathRequestMatcher(pathsToSkip, TOKEN_BASED_AUTH_ENTRY_POINT); } diff --git a/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java b/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java index 26987ac07b..41d6b33f21 100644 --- a/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java +++ b/application/src/main/java/org/thingsboard/server/controller/ApiKeyController.java @@ -46,12 +46,11 @@ import org.thingsboard.server.service.security.permission.Resource; import java.util.Optional; import java.util.UUID; -import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; import static org.thingsboard.server.controller.ControllerConstants.API_KEY_ID_PARAM_DESCRIPTION; +import static org.thingsboard.server.controller.ControllerConstants.AVAILABLE_FOR_ANY_AUTHORIZED_USER; import static org.thingsboard.server.controller.ControllerConstants.PAGE_DATA_PARAMETERS; import static org.thingsboard.server.controller.ControllerConstants.PAGE_NUMBER_DESCRIPTION; import static org.thingsboard.server.controller.ControllerConstants.PAGE_SIZE_DESCRIPTION; -import static org.thingsboard.server.controller.ControllerConstants.SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH; import static org.thingsboard.server.controller.ControllerConstants.USER_ID_PARAM_DESCRIPTION; @RestController @@ -64,8 +63,8 @@ public class ApiKeyController extends BaseController { private final ApiKeyService apiKeyService; @ApiOperation(value = "Save API key for user (saveApiKey)", - notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey '." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") + notes = "Creates an API key for the given user and returns the token ONCE as 'ApiKey '." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @PostMapping(value = "/apiKey") public ApiKey saveApiKey( @Parameter(description = "A JSON value representing the Api Key token.") @@ -73,15 +72,13 @@ public class ApiKeyController extends BaseController { SecurityUser securityUser = getCurrentUser(); apiKeyInfo.setTenantId(securityUser.getTenantId()); checkEntity(apiKeyInfo.getId(), apiKeyInfo, Resource.API_KEY); - ApiKey savedApiKey = checkNotNull(apiKeyService.saveApiKey(securityUser.getTenantId(), apiKeyInfo)); - savedApiKey.setValue(toUserApiKey(savedApiKey.getValue())); - return savedApiKey; + return checkNotNull(apiKeyService.saveApiKey(securityUser.getTenantId(), apiKeyInfo)); } @ApiOperation(value = "Get User Api Keys (getUserApiKeys)", notes = "Returns a page of api keys owned by user. " + - PAGE_DATA_PARAMETERS + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") + PAGE_DATA_PARAMETERS + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @GetMapping(value = "/apiKeys/{userId}") public PageData getUserApiKeys( @Parameter(description = USER_ID_PARAM_DESCRIPTION) @@ -100,8 +97,8 @@ public class ApiKeyController extends BaseController { @ApiOperation(value = "Update API key Description", notes = "Updates the description of the existing API key by apiKeyId. " + "Only the description can be updated. " + - "Referencing a non-existing ApiKey Id will cause a 'Not Found' error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") + "Referencing a non-existing ApiKey Id will cause a 'Not Found' error." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @PutMapping("/apiKey/{id}/description") public ApiKeyInfo updateApiKeyDescription( @Parameter(description = API_KEY_ID_PARAM_DESCRIPTION, required = true) @@ -115,8 +112,8 @@ public class ApiKeyController extends BaseController { } @ApiOperation(value = "Enable or disable API key (enableApiKey)", - notes = "Updates api key with enabled = true/false. " + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") + notes = "Updates api key with enabled = true/false. " + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @PutMapping(value = "/apiKey/{id}/enabled/{enabledValue}") public ApiKeyInfo enableApiKey( @Parameter(description = "Unique identifier of the API key to enable/disable", required = true) @@ -130,8 +127,8 @@ public class ApiKeyController extends BaseController { } @ApiOperation(value = "Delete API key by ID (deleteApiKey)", - notes = "Deletes the API key. Referencing non-existing ApiKey Id will cause an error." + SYSTEM_OR_TENANT_AUTHORITY_PARAGRAPH) - @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN')") + notes = "Deletes the API key. Referencing non-existing ApiKey Id will cause an error." + AVAILABLE_FOR_ANY_AUTHORIZED_USER) + @PreAuthorize("hasAnyAuthority('SYS_ADMIN','TENANT_ADMIN', 'CUSTOMER_USER')") @DeleteMapping(value = "/apiKey/{id}") public void deleteApiKey(@PathVariable UUID id) throws ThingsboardException { ApiKeyId apiKeyId = new ApiKeyId(id); @@ -139,8 +136,4 @@ public class ApiKeyController extends BaseController { apiKeyService.deleteApiKey(apiKey.getTenantId(), apiKey, false); } - private String toUserApiKey(String value) { - return API_KEY_HEADER_PREFIX + value; - } - } diff --git a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java index 03ab77ac09..07e96dd897 100644 --- a/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java +++ b/application/src/main/java/org/thingsboard/server/service/entitiy/EntityStateSourcingListener.java @@ -31,6 +31,7 @@ import org.thingsboard.server.common.data.TbResource; import org.thingsboard.server.common.data.TbResourceInfo; import org.thingsboard.server.common.data.Tenant; import org.thingsboard.server.common.data.TenantProfile; +import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.asset.Asset; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.cf.CalculatedField; @@ -54,6 +55,7 @@ import org.thingsboard.server.common.msg.edge.EdgeEventUpdateMsg; import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; import org.thingsboard.server.common.msg.rule.engine.DeviceCredentialsUpdateNotificationMsg; import org.thingsboard.server.dao.edge.EdgeSynchronizationManager; +import org.thingsboard.server.dao.eventsourcing.ActionCause; import org.thingsboard.server.dao.eventsourcing.ActionEntityEvent; import org.thingsboard.server.dao.eventsourcing.DeleteEntityEvent; import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; @@ -99,7 +101,7 @@ public class EntityStateSourcingListener { case ASSET -> { onAssetUpdate(event.getEntity(), event.getOldEntity()); } - case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE -> { + case ASSET_PROFILE, ENTITY_VIEW, NOTIFICATION_RULE, USER -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, lifecycleEvent); } case RULE_CHAIN -> { @@ -164,7 +166,7 @@ public class EntityStateSourcingListener { Asset asset = (Asset) event.getEntity(); tbClusterService.onAssetDeleted(tenantId, asset, null); } - case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE -> { + case ASSET_PROFILE, ENTITY_VIEW, CUSTOMER, EDGE, NOTIFICATION_RULE, USER -> { tbClusterService.broadcastEntityStateChangeEvent(tenantId, entityId, ComponentLifecycleEvent.DELETED); } case NOTIFICATION_REQUEST -> { @@ -228,6 +230,8 @@ public class EntityStateSourcingListener { tbClusterService.onDeviceAssignedToTenant(tenant.getId(), device); } pushAssignedFromNotification(tenant, event.getTenantId(), device); + } else if (event.getActionType() == ActionType.CREDENTIALS_UPDATED && event.getEntityId() != null && event.getEntityId().getEntityType() == EntityType.USER) { + tbClusterService.broadcastEntityStateChangeEvent(event.getTenantId(), event.getEntityId(), ComponentLifecycleEvent.UPDATED); } } diff --git a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java index 265f14c4e2..2d5f43420f 100644 --- a/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java +++ b/application/src/main/java/org/thingsboard/server/service/queue/DefaultTbClusterService.java @@ -592,7 +592,8 @@ public class DefaultTbClusterService implements TbClusterService { EntityType.TENANT_PROFILE, EntityType.DEVICE_PROFILE, EntityType.ASSET_PROFILE, - EntityType.JOB) + EntityType.JOB, + EntityType.USER) || (entityType == EntityType.ASSET && msg.getEvent() == ComponentLifecycleEvent.UPDATED) || (entityType == EntityType.DEVICE && msg.getEvent() == ComponentLifecycleEvent.UPDATED) ) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/AbstractHeaderTokenExtractor.java b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/AbstractHeaderTokenExtractor.java index d8bd0834e1..cafb7619a7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/AbstractHeaderTokenExtractor.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/extractor/AbstractHeaderTokenExtractor.java @@ -30,9 +30,9 @@ public abstract class AbstractHeaderTokenExtractor implements TokenExtractor { @Override public String extract(HttpServletRequest request) { - String header = request.getHeader(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM); + String header = request.getHeader(ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER); if (StringUtils.isBlank(header)) { - header = request.getHeader(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM_V2); + header = request.getHeader(ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER_V2); if (StringUtils.isBlank(header)) { throw new AuthenticationServiceException("Authorization header cannot be blank!"); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java index e66741f749..3c00f09ab7 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/JwtTokenAuthenticationProcessingFilter.java @@ -33,9 +33,9 @@ import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; import java.io.IOException; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER_V2; import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.BEARER_HEADER_PREFIX; -import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM; -import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM_V2; public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { @@ -70,9 +70,9 @@ public class JwtTokenAuthenticationProcessingFilter extends AbstractAuthenticati if (!super.requiresAuthentication(request, response)) { return false; } - String header = request.getHeader(JWT_TOKEN_HEADER_PARAM); + String header = request.getHeader(AUTHORIZATION_HEADER); if (header == null) { - header = request.getHeader(JWT_TOKEN_HEADER_PARAM_V2); + header = request.getHeader(AUTHORIZATION_HEADER_V2); } if (header == null) { // If there is NO auth header at all, let the JWT filter try to attempt Authentication and failure in the process. diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java index 7c11bd879b..6db0350bad 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/jwt/RefreshTokenAuthenticationProvider.java @@ -28,28 +28,29 @@ import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.thingsboard.server.common.data.Customer; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserAuthDetails; 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.id.UserId; import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.customer.CustomerService; -import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.RefreshAuthenticationToken; import org.thingsboard.server.service.security.auth.TokenOutdatingService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; +import org.thingsboard.server.service.user.cache.UserAuthDetailsCache; import java.util.UUID; @Component @RequiredArgsConstructor public class RefreshTokenAuthenticationProvider implements AuthenticationProvider { + private final JwtTokenFactory tokenFactory; - private final UserService userService; + private final UserAuthDetailsCache userEnabledCache; private final CustomerService customerService; private final TokenOutdatingService tokenOutdatingService; @@ -61,7 +62,7 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide UserPrincipal principal = unsafeUser.getUserPrincipal(); SecurityUser securityUser; - if (principal.getType() == UserPrincipal.Type.USER_NAME) { + if (principal.getType() == UserPrincipal.Type.USER_NAME) { securityUser = authenticateByUserId(unsafeUser.getId()); } else { securityUser = authenticateByPublicId(principal.getValue()); @@ -75,26 +76,21 @@ public class RefreshTokenAuthenticationProvider implements AuthenticationProvide } private SecurityUser authenticateByUserId(UserId userId) { - TenantId systemId = TenantId.SYS_TENANT_ID; - User user = userService.findUserById(systemId, userId); - if (user == null) { - throw new UsernameNotFoundException("User not found by refresh token"); - } - - UserCredentials userCredentials = userService.findUserCredentialsByUserId(systemId, user.getId()); - if (userCredentials == null) { - throw new UsernameNotFoundException("User credentials not found"); + UserAuthDetails userAuthDetails = userEnabledCache.findUserEnabled(TenantId.SYS_TENANT_ID, userId); + if (userAuthDetails == null) { + throw new UsernameNotFoundException("User with credentials not found"); } - - if (!userCredentials.isEnabled()) { + if (!userAuthDetails.credentialsEnabled()) { throw new DisabledException("User is not active"); } - if (user.getAuthority() == null) throw new InsufficientAuthenticationException("User has no authority assigned"); + User user = userAuthDetails.user(); + if (user.getAuthority() == null) { + throw new InsufficientAuthenticationException("User has no authority assigned"); + } UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); - - return new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); + return new SecurityUser(user, true, userPrincipal); } private SecurityUser authenticateByPublicId(String publicId) { diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java index 68a676ef18..86884a2eb4 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProvider.java @@ -26,27 +26,25 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.UserAuthDetails; import org.thingsboard.server.common.data.pat.ApiKey; -import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.pat.ApiKeyService; -import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; -import org.thingsboard.server.service.security.model.token.RawApiKeyToken; +import org.thingsboard.server.service.security.model.token.RawApiKey; +import org.thingsboard.server.service.user.cache.UserAuthDetailsCache; @Component @RequiredArgsConstructor public class ApiKeyAuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider { private final ApiKeyService apiKeyService; - private final UserService userService; + private final UserAuthDetailsCache userEnabledCache; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { - RawApiKeyToken raw = (RawApiKeyToken) authentication.getCredentials(); - SecurityUser securityUser = authenticate(raw.token()); + RawApiKey rawApiKey = (RawApiKey) authentication.getCredentials(); + SecurityUser securityUser = authenticate(rawApiKey.apiKey()); return new ApiKeyAuthenticationToken(securityUser); } @@ -69,26 +67,20 @@ public class ApiKeyAuthenticationProvider implements org.springframework.securit if (apiKey.getExpirationTime() != 0 && apiKey.getExpirationTime() < System.currentTimeMillis()) { throw new CredentialsExpiredException("API key is expired"); } - TenantId tenantId = apiKey.getTenantId(); - UserId userId = apiKey.getUserId(); - User user = userService.findUserById(tenantId, userId); - if (user == null) { - throw new UsernameNotFoundException("User for the provided API key is no longer exists"); + UserAuthDetails userAuthDetails = userEnabledCache.findUserEnabled(apiKey.getTenantId(), apiKey.getUserId()); + if (userAuthDetails == null) { + throw new UsernameNotFoundException("User with credentials not found"); } - UserCredentials userCredentials = userService.findUserCredentialsByUserId(tenantId, userId); - if (userCredentials == null) { - throw new UsernameNotFoundException("User credentials not found"); - } - if (!userCredentials.isEnabled()) { + if (!userAuthDetails.credentialsEnabled()) { throw new DisabledException("User is not active"); } + + User user = userAuthDetails.user(); if (user.getAuthority() == null) { throw new InsufficientAuthenticationException("User has no authority assigned"); } - UserPrincipal userPrincipal = new UserPrincipal(UserPrincipal.Type.USER_NAME, user.getEmail()); - - return new SecurityUser(user, userCredentials.isEnabled(), userPrincipal); + return new SecurityUser(user, true, userPrincipal); } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationToken.java b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationToken.java index c6f686f204..8165baf483 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationToken.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationToken.java @@ -17,7 +17,7 @@ package org.thingsboard.server.service.security.auth.pat; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.thingsboard.server.service.security.model.SecurityUser; -import org.thingsboard.server.service.security.model.token.RawApiKeyToken; +import org.thingsboard.server.service.security.model.token.RawApiKey; import java.io.Serial; @@ -26,12 +26,12 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { @Serial private static final long serialVersionUID = 2978710889397403536L; - private RawApiKeyToken rawApiKeyToken; + private RawApiKey rawApiKey; private SecurityUser securityUser; - public ApiKeyAuthenticationToken(RawApiKeyToken raw) { + public ApiKeyAuthenticationToken(RawApiKey rawApiKey) { super(null); - this.rawApiKeyToken = raw; + this.rawApiKey = rawApiKey; setAuthenticated(false); } @@ -44,7 +44,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { @Override public Object getCredentials() { - return rawApiKeyToken; + return rawApiKey; } @Override @@ -55,7 +55,7 @@ public class ApiKeyAuthenticationToken extends AbstractAuthenticationToken { @Override public void eraseCredentials() { super.eraseCredentials(); - this.rawApiKeyToken = null; + this.rawApiKey = null; } } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyTokenAuthenticationProcessingFilter.java b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyTokenAuthenticationProcessingFilter.java index b5f940f4cc..20a95a5ae5 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyTokenAuthenticationProcessingFilter.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/pat/ApiKeyTokenAuthenticationProcessingFilter.java @@ -29,13 +29,13 @@ import org.springframework.security.web.authentication.AbstractAuthenticationPro import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.util.matcher.RequestMatcher; import org.thingsboard.server.service.security.auth.extractor.TokenExtractor; -import org.thingsboard.server.service.security.model.token.RawApiKeyToken; +import org.thingsboard.server.service.security.model.token.RawApiKey; import java.io.IOException; import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; -import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM; -import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM_V2; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER_V2; public class ApiKeyTokenAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter { @@ -52,8 +52,8 @@ public class ApiKeyTokenAuthenticationProcessingFilter extends AbstractAuthentic @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { - RawApiKeyToken token = new RawApiKeyToken(tokenExtractor.extract(request)); - return getAuthenticationManager().authenticate(new ApiKeyAuthenticationToken(token)); + RawApiKey rawApiKey = new RawApiKey(tokenExtractor.extract(request)); + return getAuthenticationManager().authenticate(new ApiKeyAuthenticationToken(rawApiKey)); } @Override @@ -70,9 +70,9 @@ public class ApiKeyTokenAuthenticationProcessingFilter extends AbstractAuthentic if (!super.requiresAuthentication(request, response)) { return false; } - String header = request.getHeader(JWT_TOKEN_HEADER_PARAM); + String header = request.getHeader(AUTHORIZATION_HEADER); if (header == null) { - header = request.getHeader(JWT_TOKEN_HEADER_PARAM_V2); + header = request.getHeader(AUTHORIZATION_HEADER_V2); } return header != null && header.startsWith(API_KEY_HEADER_PREFIX); } diff --git a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java index 510278b25a..4450e624ec 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java +++ b/application/src/main/java/org/thingsboard/server/service/security/auth/rest/RestAuthenticationProvider.java @@ -82,11 +82,10 @@ public class RestAuthenticationProvider implements AuthenticationProvider { Assert.notNull(authentication, "No authentication data provided"); Object principal = authentication.getPrincipal(); - if (!(principal instanceof UserPrincipal)) { + if (!(principal instanceof UserPrincipal userPrincipal)) { throw new BadCredentialsException("Authentication Failed. Bad user principal."); } - UserPrincipal userPrincipal = (UserPrincipal) principal; SecurityUser securityUser; if (userPrincipal.getType() == UserPrincipal.Type.USER_NAME) { String username = userPrincipal.getValue(); diff --git a/application/src/main/java/org/thingsboard/server/service/security/model/token/RawApiKeyToken.java b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawApiKey.java similarity index 93% rename from application/src/main/java/org/thingsboard/server/service/security/model/token/RawApiKeyToken.java rename to application/src/main/java/org/thingsboard/server/service/security/model/token/RawApiKey.java index e361c48bc8..1268df592f 100644 --- a/application/src/main/java/org/thingsboard/server/service/security/model/token/RawApiKeyToken.java +++ b/application/src/main/java/org/thingsboard/server/service/security/model/token/RawApiKey.java @@ -15,6 +15,4 @@ */ package org.thingsboard.server.service.security.model.token; -public record RawApiKeyToken(String token) { - -} +public record RawApiKey(String apiKey) {} diff --git a/application/src/main/java/org/thingsboard/server/service/ttl/ApiKeysCleanUpService.java b/application/src/main/java/org/thingsboard/server/service/ttl/ApiKeysCleanUpService.java index b394b93c09..c677db7108 100644 --- a/application/src/main/java/org/thingsboard/server/service/ttl/ApiKeysCleanUpService.java +++ b/application/src/main/java/org/thingsboard/server/service/ttl/ApiKeysCleanUpService.java @@ -16,7 +16,6 @@ package org.thingsboard.server.service.ttl; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; @@ -24,8 +23,6 @@ import org.thingsboard.server.dao.pat.ApiKeyDao; import org.thingsboard.server.queue.discovery.PartitionService; import org.thingsboard.server.queue.util.TbCoreComponent; -import java.util.concurrent.TimeUnit; - @Slf4j @Service @TbCoreComponent @@ -35,9 +32,6 @@ public class ApiKeysCleanUpService extends AbstractCleanUpService { public static final String RANDOM_DELAY_INTERVAL_MS_EXPRESSION = "#{T(org.apache.commons.lang3.RandomUtils).nextLong(0, ${sql.ttl.api_keys.checking_interval_ms})}"; - @Value("${sql.ttl.api_keys.ttl:2592000}") - private long ttl; - private final ApiKeyDao apiKeyDao; public ApiKeysCleanUpService(PartitionService partitionService, ApiKeyDao apiKeyDao) { @@ -50,7 +44,7 @@ public class ApiKeysCleanUpService extends AbstractCleanUpService { fixedDelayString = "${sql.ttl.api_keys.checking_interval_ms:86400000}" ) public void cleanUp() { - long threshold = System.currentTimeMillis() - TimeUnit.SECONDS.toMillis(ttl); + long threshold = System.currentTimeMillis(); if (isSystemTenantPartitionMine()) { int deleted = apiKeyDao.deleteAllByExpirationTimeBefore(threshold); if (deleted > 0) { diff --git a/application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java b/application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java new file mode 100644 index 0000000000..21c9c9069d --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/user/cache/DefaultUserAuthDetailsCache.java @@ -0,0 +1,85 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.user.cache; + +import com.github.benmanes.caffeine.cache.Cache; +import com.github.benmanes.caffeine.cache.Caffeine; +import jakarta.annotation.PostConstruct; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Service; +import org.thingsboard.server.common.data.EntityType; +import org.thingsboard.server.common.data.UserAuthDetails; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.msg.plugin.ComponentLifecycleMsg; +import org.thingsboard.server.dao.user.UserService; + +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReadWriteLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DefaultUserAuthDetailsCache implements UserAuthDetailsCache { + + private final UserService userService; + + @Value("${cache.userEnabled.maxSize:1000}") + private int cacheMaxSize; + @Value("${cache.userEnabled.timeToLiveInMinutes:30}") + private int cacheValueTtl; + private Cache cache; + + private final ReadWriteLock lock = new ReentrantReadWriteLock(); + + @PostConstruct + private void init() { + cache = Caffeine.newBuilder() + .maximumSize(cacheMaxSize) + .expireAfterAccess(cacheValueTtl, TimeUnit.MINUTES) + .build(); + } + + @EventListener(ComponentLifecycleMsg.class) + public void onComponentLifecycleEvent(ComponentLifecycleMsg event) { + if (event.getEntityId() != null) { + if (event.getEntityId().getEntityType() == EntityType.USER) { + evict(new UserId(event.getEntityId().getId())); + } + } + } + + @Override + public UserAuthDetails findUserEnabled(TenantId tenantId, UserId userId) { + lock.readLock().lock(); + try { + log.trace("Retrieving user with enabled credentials status with id {} for tenant {} from cache", userId, tenantId); + return cache.get(userId, id -> userService.findUserAuthDetailsByUserId(tenantId, id)); + } finally { + lock.readLock().unlock(); + } + } + + public void evict(UserId userId) { + cache.invalidate(userId); + log.trace("Evicted record for user {} from cache", userId); + } + +} diff --git a/application/src/main/java/org/thingsboard/server/service/user/cache/UserAuthDetailsCache.java b/application/src/main/java/org/thingsboard/server/service/user/cache/UserAuthDetailsCache.java new file mode 100644 index 0000000000..6363ccae67 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/user/cache/UserAuthDetailsCache.java @@ -0,0 +1,26 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.service.user.cache; + +import org.thingsboard.server.common.data.UserAuthDetails; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; + +public interface UserAuthDetailsCache { + + UserAuthDetails findUserEnabled(TenantId tenantId, UserId userId); + +} diff --git a/application/src/main/resources/thingsboard.yml b/application/src/main/resources/thingsboard.yml index 361c584492..8884a66cc8 100644 --- a/application/src/main/resources/thingsboard.yml +++ b/application/src/main/resources/thingsboard.yml @@ -151,6 +151,12 @@ security: tokenSigningKey: "${JWT_TOKEN_SIGNING_KEY:thingsboardDefaultSigningKey}" # Base64 encoded # Enable/disable access to Tenant Administrators JWT token by System Administrator or Customer Users JWT token by Tenant Administrator user_token_access_enabled: "${SECURITY_USER_TOKEN_ACCESS_ENABLED:true}" + # API key parameters + api_key: + # Prefix for the auto-generated API key. For example, tb_Ood4dQMxWvMH-76z3E_Cv0mZaBWT0Clk3hRSO0P_jNQ + value_prefix: "${SECURITY_API_KEY_VALUE_PREFIX:tb_}" + # Length of the auto-generated API key. Max is 255 + value_bytes_size: "${SECURITY_API_KEY_VALUE_PREFIX:64}" # Enable/disable case-sensitive username login user_login_case_sensitive: "${SECURITY_USER_LOGIN_CASE_SENSITIVE:true}" claim: @@ -429,7 +435,6 @@ sql: checking_interval_ms: "${SQL_TTL_NOTIFICATIONS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day api_keys: enabled: "${SQL_TTL_API_KEYS_ENABLED:true}" # Enable/disable TTL (Time To Live) for api keys records - ttl: "${SQL_TTL_API_KEYS_SECS:2592000}" # Default value - 30 days checking_interval_ms: "${SQL_TTL_API_KEYS_CHECKING_INTERVAL_MS:86400000}" # Default value - 1 day relations: max_level: "${SQL_RELATIONS_MAX_LEVEL:50}" # This value has to be reasonably small to prevent infinite recursion as early as possible @@ -666,6 +671,9 @@ cache: aiModel: timeToLiveInMinutes: "${CACHE_SPECS_AI_MODEL_TTL:1440}" # AI model cache TTL maxSize: "${CACHE_SPECS_AI_MODEL_MAX_SIZE:10000}" # 0 means the cache is disabled + apiKeys: + timeToLiveInMinutes: "${CACHE_SPECS_API_KEYS_TTL:1440}" # API keys cache TTL + maxSize: "${CACHE_SPECS_API_KEYS_MAX_SIZE:10000}" # 0 means the cache is disabled # Deliberately placed outside the 'specs' group above notificationRules: @@ -683,6 +691,9 @@ cache: maxSize: "${CACHE_SPECS_IMAGE_ETAGS_MAX_SIZE:10000}" # 0 means the cache is disabled systemImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_SYSTEM_BROWSER_TTL:0}" # Browser cache TTL for system images in minutes. 0 means the cache is disabled tenantImagesBrowserTtlInMinutes: "${CACHE_SPECS_IMAGE_TENANT_BROWSER_TTL:0}" # Browser cache TTL for tenant images in minutes. 0 means the cache is disabled + userEnabled: + timeToLiveInMinutes: "${CACHE_SPECS_USER_ENABLED_TTL:120}" # User enabled cache TTL + maxSize: "${CACHE_SPECS_USER_ENABLED_MAX_SIZE:200000}" # 0 means the cache is disabled # Spring data parameters spring.data.redis.repositories.enabled: false # Disable this because it is not required. diff --git a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java index fd01581e36..6da301062c 100644 --- a/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/AbstractWebTest.java @@ -192,6 +192,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; import static org.thingsboard.server.common.data.CacheConstants.CLAIM_DEVICES_CACHE; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; +import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.BEARER_HEADER_PREFIX; @Slf4j public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { @@ -237,6 +239,8 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected String mobileToken; protected String username; + protected String apiKey; + protected TenantId tenantId; protected TenantProfileId tenantProfileId; protected UserId tenantAdminUserId; @@ -631,13 +635,27 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected void setJwtToken(MockHttpServletRequestBuilder request) { if (this.token != null) { - request.header(ThingsboardSecurityConfiguration.JWT_TOKEN_HEADER_PARAM, "Bearer " + this.token); + request.header(ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER, BEARER_HEADER_PREFIX + this.token); } if (this.mobileToken != null) { request.header(UserController.MOBILE_TOKEN_HEADER, this.mobileToken); } } + protected void resetApiKey() { + this.apiKey = null; + } + + protected void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + protected void setApiKey(MockHttpServletRequestBuilder request) { + if (this.apiKey != null) { + request.header(ThingsboardSecurityConfiguration.AUTHORIZATION_HEADER, API_KEY_HEADER_PREFIX + this.apiKey); + } + } + protected DeviceProfile createDeviceProfile(String name) { return createDeviceProfile(name, null); } @@ -744,6 +762,12 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(getRequest); } + protected ResultActions doGetWithApiKey(String urlTemplate, Object... urlVariables) throws Exception { + MockHttpServletRequestBuilder getRequest = get(urlTemplate, urlVariables); + setApiKey(getRequest); + return mockMvc.perform(getRequest); + } + protected T doGet(String urlTemplate, Class responseClass, Object... urlVariables) throws Exception { return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); } @@ -767,6 +791,10 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(asyncDispatch(mockMvc.perform(getRequest).andExpect(request().asyncStarted()).andReturn())); } + protected T doGetWithApiKey(String urlTemplate, Class responseClass, Object... urlVariables) throws Exception { + return readResponse(doGetWithApiKey(urlTemplate, urlVariables).andExpect(status().isOk()), responseClass); + } + protected T doGetTyped(String urlTemplate, TypeReference responseType, Object... urlVariables) throws Exception { return readResponse(doGet(urlTemplate, urlVariables).andExpect(status().isOk()), responseType); } @@ -846,6 +874,14 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { } } + protected R doPostWithApiKey(String urlTemplate, T content, Class responseClass, String... params) { + try { + return readResponse(doPostWithApiKey(urlTemplate, content, params).andExpect(status().isOk()), responseClass); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + protected R doPostWithResponse(String urlTemplate, T content, Class responseClass, String... params) throws Exception { return readResponse(doPost(urlTemplate, content, params).andExpect(status().isOk()), responseClass); } @@ -913,6 +949,14 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(postRequest); } + protected ResultActions doPostWithApiKey(String urlTemplate, T content, String... params) throws Exception { + MockHttpServletRequestBuilder postRequest = post(urlTemplate, params); + setApiKey(postRequest); + String json = json(content); + postRequest.contentType(contentType).content(json); + return mockMvc.perform(postRequest); + } + protected ResultActions doPostAsync(String urlTemplate, T content, Long timeout, String... params) throws Exception { MockHttpServletRequestBuilder postRequest = post(urlTemplate, params); setJwtToken(postRequest); @@ -930,6 +974,13 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { return mockMvc.perform(deleteRequest); } + protected ResultActions doDeleteWithApiKey(String urlTemplate, String... params) throws Exception { + MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate); + setApiKey(deleteRequest); + populateParams(deleteRequest, params); + return mockMvc.perform(deleteRequest); + } + protected ResultActions doDeleteAsync(String urlTemplate, Long timeout, String... params) throws Exception { MockHttpServletRequestBuilder deleteRequest = delete(urlTemplate, params); setJwtToken(deleteRequest); @@ -1300,7 +1351,7 @@ public abstract class AbstractWebTest extends AbstractInMemoryStorageTest { protected List findJobs(List types, List entities) throws Exception { return doGetTypedWithPageLink("/api/jobs?types=" + types.stream().map(Enum::name).collect(Collectors.joining(",")) + - "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", + "&entities=" + entities.stream().map(UUID::toString).collect(Collectors.joining(",")) + "&", new TypeReference>() {}, new PageLink(100, 0, null, new SortOrder("createdTime", SortOrder.Direction.DESC))).getData(); } diff --git a/application/src/test/java/org/thingsboard/server/controller/ApiKeyControllerTest.java b/application/src/test/java/org/thingsboard/server/controller/ApiKeyControllerTest.java index d71d73b6df..167839e7f3 100644 --- a/application/src/test/java/org/thingsboard/server/controller/ApiKeyControllerTest.java +++ b/application/src/test/java/org/thingsboard/server/controller/ApiKeyControllerTest.java @@ -28,7 +28,6 @@ import org.thingsboard.server.dao.service.DaoSqlTest; import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import static org.thingsboard.server.config.ThingsboardSecurityConfiguration.API_KEY_HEADER_PREFIX; @DaoSqlTest public class ApiKeyControllerTest extends AbstractControllerTest { @@ -42,8 +41,7 @@ public class ApiKeyControllerTest extends AbstractControllerTest { public void testSaveApiKey() throws Exception { ApiKeyInfo apiKeyInfo = constructApiKeyInfo("New API key description", true); - String apiKeyStr = doPost("/api/apiKey", apiKeyInfo, ApiKey.class).getValue(); - Assert.assertTrue(apiKeyStr.startsWith(API_KEY_HEADER_PREFIX)); + doPost("/api/apiKey", apiKeyInfo, ApiKey.class); PageData pageData = doGetTypedWithPageLink("/api/apiKeys/" + tenantAdminUserId + "?", new TypeReference<>() {}, new PageLink(10, 0)); Assert.assertEquals(1, pageData.getData().size()); diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java index 49e4ed706c..3726a0acff 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/TokenOutdatingTest.java @@ -30,15 +30,14 @@ import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.junit4.SpringRunner; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserAuthDetails; 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.common.data.security.event.UserCredentialsInvalidationEvent; import org.thingsboard.server.common.data.security.event.UserSessionInvalidationEvent; import org.thingsboard.server.common.data.security.model.JwtToken; import org.thingsboard.server.dao.customer.CustomerService; import org.thingsboard.server.dao.service.DaoSqlTest; -import org.thingsboard.server.dao.user.UserService; import org.thingsboard.server.service.security.auth.jwt.JwtAuthenticationProvider; import org.thingsboard.server.service.security.auth.jwt.RefreshTokenAuthenticationProvider; import org.thingsboard.server.service.security.exception.JwtExpiredTokenException; @@ -46,6 +45,7 @@ import org.thingsboard.server.service.security.model.SecurityUser; import org.thingsboard.server.service.security.model.UserPrincipal; import org.thingsboard.server.service.security.model.token.JwtTokenFactory; import org.thingsboard.server.service.security.model.token.RawAccessJwtToken; +import org.thingsboard.server.service.user.cache.UserAuthDetailsCache; import java.util.UUID; @@ -91,20 +91,16 @@ public class TokenOutdatingTest { UserId userId = new UserId(UUID.randomUUID()); securityUser = createMockSecurityUser(userId); - UserService userService = mock(UserService.class); + UserAuthDetailsCache userAuthDetailsCache = mock(UserAuthDetailsCache.class); User user = new User(); user.setId(userId); user.setAuthority(Authority.TENANT_ADMIN); user.setEmail("email"); - when(userService.findUserById(any(), eq(userId))).thenReturn(user); - - UserCredentials userCredentials = new UserCredentials(); - userCredentials.setEnabled(true); - when(userService.findUserCredentialsByUserId(any(), eq(userId))).thenReturn(userCredentials); + when(userAuthDetailsCache.findUserEnabled(any(), eq(userId))).thenReturn(new UserAuthDetails(user, true)); accessTokenAuthenticationProvider = new JwtAuthenticationProvider(tokenFactory, tokenOutdatingService); - refreshTokenAuthenticationProvider = new RefreshTokenAuthenticationProvider(tokenFactory, userService, mock(CustomerService.class), tokenOutdatingService); + refreshTokenAuthenticationProvider = new RefreshTokenAuthenticationProvider(tokenFactory, userAuthDetailsCache, mock(CustomerService.class), tokenOutdatingService); } @Test diff --git a/application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java b/application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java index 00fac8866a..3c1b7dda18 100644 --- a/application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java +++ b/application/src/test/java/org/thingsboard/server/service/security/auth/pat/ApiKeyAuthenticationProviderTest.java @@ -15,176 +15,98 @@ */ package org.thingsboard.server.service.security.auth.pat; +import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.security.authentication.BadCredentialsException; -import org.springframework.security.authentication.CredentialsExpiredException; -import org.springframework.security.authentication.DisabledException; -import org.springframework.security.authentication.InsufficientAuthenticationException; -import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UsernameNotFoundException; -import org.thingsboard.server.common.data.User; -import org.thingsboard.server.common.data.id.ApiKeyId; -import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserId; +import org.mockito.Mockito; +import org.thingsboard.server.common.data.audit.ActionType; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.pat.ApiKey; -import org.thingsboard.server.common.data.security.Authority; -import org.thingsboard.server.common.data.security.UserCredentials; -import org.thingsboard.server.dao.pat.ApiKeyService; -import org.thingsboard.server.dao.user.UserService; -import org.thingsboard.server.service.security.model.SecurityUser; -import org.thingsboard.server.service.security.model.token.RawApiKeyToken; +import org.thingsboard.server.common.data.pat.ApiKeyInfo; +import org.thingsboard.server.controller.AbstractControllerTest; +import org.thingsboard.server.dao.service.DaoSqlTest; -import java.util.UUID; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.model.ModelConstants.NULL_UUID; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.when; +@DaoSqlTest +public class ApiKeyAuthenticationProviderTest extends AbstractControllerTest { -@RunWith(MockitoJUnitRunner.class) -public class ApiKeyAuthenticationProviderTest { - - private static final String TEST_API_KEY = "test_api_key"; - private static final String USER_EMAIL = "tenant@thingsboard.org"; - - @Mock - private ApiKeyService apiKeyService; - - @Mock - private UserService userService; - - private ApiKeyAuthenticationProvider provider; - private TenantId tenantId; - private UserId userId; - private User user; - private UserCredentials userCredentials; - private ApiKey apiKey; + ApiKey savedApiKey; @Before - public void setUp() { - provider = new ApiKeyAuthenticationProvider(apiKeyService, userService); - tenantId = TenantId.fromUUID(UUID.randomUUID()); - userId = new UserId(UUID.randomUUID()); - - user = new User(); - user.setId(userId); - user.setTenantId(tenantId); - user.setEmail(USER_EMAIL); - user.setAuthority(Authority.TENANT_ADMIN); - - userCredentials = new UserCredentials(); - userCredentials.setEnabled(true); - - apiKey = new ApiKey(); - apiKey.setId(new ApiKeyId(UUID.randomUUID())); - apiKey.setTenantId(tenantId); - apiKey.setUserId(userId); - apiKey.setValue(TEST_API_KEY); - apiKey.setEnabled(true); - apiKey.setExpirationTime(0); - } + public void setUp() throws Exception { + loginTenantAdmin(); - @Test - public void testSuccessfulAuthentication() { - when(apiKeyService.findApiKeyByValue(TEST_API_KEY)).thenReturn(apiKey); - when(userService.findUserById(tenantId, userId)).thenReturn(user); - when(userService.findUserCredentialsByUserId(tenantId, userId)).thenReturn(userCredentials); - - ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); - - Authentication authentication = provider.authenticate(token); - - assertNotNull(authentication); - assertTrue(authentication.isAuthenticated()); - assertTrue(authentication instanceof ApiKeyAuthenticationToken); - SecurityUser securityUser = (SecurityUser) authentication.getPrincipal(); - assertEquals(userId, securityUser.getId()); - assertEquals(tenantId, securityUser.getTenantId()); - assertEquals(USER_EMAIL, securityUser.getEmail()); - assertEquals(Authority.TENANT_ADMIN, securityUser.getAuthority()); + ApiKeyInfo apiKeyInfo = constructApiKeyInfo(); + savedApiKey = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + setApiKey(savedApiKey.getValue()); } - @Test(expected = BadCredentialsException.class) - public void testEmptyApiKey() { - ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken("")); - - provider.authenticate(token); + @After + public void cleanUp() throws Exception { + resetApiKey(); + doDelete("/api/apiKey/" + savedApiKey.getId()).andExpect(status().isOk()); } - @Test(expected = BadCredentialsException.class) - public void testNonExistentApiKey() { - when(apiKeyService.findApiKeyByValue(TEST_API_KEY)).thenReturn(null); - ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); - - provider.authenticate(token); - } + @Test + public void testSaveEdgeWithApiKey() throws Exception { + Edge edge = constructEdge("My edge", "default"); - @Test(expected = DisabledException.class) - public void testDisabledApiKey() { - apiKey.setEnabled(false); - when(apiKeyService.findApiKeyByValue(TEST_API_KEY)).thenReturn(apiKey); - ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); + Mockito.reset(tbClusterService, auditLogService); - provider.authenticate(token); - } + Edge savedEdge = doPostWithApiKey("/api/edge", edge, Edge.class); - @Test(expected = CredentialsExpiredException.class) - public void testExpiredApiKey() { - apiKey.setExpirationTime(System.currentTimeMillis() - 10000); // Expired 10 seconds ago - when(apiKeyService.findApiKeyByValue(TEST_API_KEY)).thenReturn(apiKey); - ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); + Assert.assertNotNull(savedEdge); + Assert.assertNotNull(savedEdge.getId()); + Assert.assertTrue(savedEdge.getCreatedTime() > 0); + Assert.assertEquals(tenantId, savedEdge.getTenantId()); + Assert.assertNotNull(savedEdge.getCustomerId()); + Assert.assertEquals(NULL_UUID, savedEdge.getCustomerId().getId()); + Assert.assertEquals(edge.getName(), savedEdge.getName()); - provider.authenticate(token); - } + testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(savedEdge, savedEdge.getId(), savedEdge.getId(), + tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), + ActionType.ADDED, 2); - @Test(expected = UsernameNotFoundException.class) - public void testNonExistentUser() { - when(apiKeyService.findApiKeyByValue(TEST_API_KEY)).thenReturn(apiKey); - when(userService.findUserById(tenantId, userId)).thenReturn(null); - ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); + savedEdge.setName("My new edge"); + doPostWithApiKey("/api/edge", savedEdge, Edge.class); - provider.authenticate(token); - } + Edge foundEdge = doGetWithApiKey("/api/edge/" + savedEdge.getId().getId().toString(), Edge.class); + Assert.assertEquals(foundEdge.getName(), savedEdge.getName()); - @Test(expected = UsernameNotFoundException.class) - public void testNonExistentUserCredentials() { - when(apiKeyService.findApiKeyByValue(TEST_API_KEY)).thenReturn(apiKey); - when(userService.findUserById(tenantId, userId)).thenReturn(user); - when(userService.findUserCredentialsByUserId(tenantId, userId)).thenReturn(null); - ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); + testNotifyEdgeStateChangeEventManyTimeMsgToEdgeServiceNever(foundEdge, foundEdge.getId(), foundEdge.getId(), + tenantId, tenantAdminUser.getCustomerId(), tenantAdminUser.getId(), tenantAdminUser.getEmail(), + ActionType.UPDATED, 1); - provider.authenticate(token); + doDeleteWithApiKey("/api/edge/" + savedEdge.getId().getId().toString()) + .andExpect(status().isOk()); } - @Test(expected = DisabledException.class) - public void testDisabledUser() { - userCredentials.setEnabled(false); - when(apiKeyService.findApiKeyByValue(TEST_API_KEY)).thenReturn(apiKey); - when(userService.findUserById(tenantId, userId)).thenReturn(user); - when(userService.findUserCredentialsByUserId(tenantId, userId)).thenReturn(userCredentials); - ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); - - provider.authenticate(token); + @Test + public void testUnauthorizedWhenKeyDisabled() throws Exception { + ApiKeyInfo disabledApiKeyInfo = doPut("/api/apiKey/" + savedApiKey.getId().getId() + "/enabled/false", Boolean.FALSE, ApiKeyInfo.class); + Assert.assertFalse(disabledApiKeyInfo.isEnabled()); + doGetWithApiKey("/api/admin/featuresInfo").andExpect(status().isUnauthorized()); } - @Test(expected = InsufficientAuthenticationException.class) - public void testUserWithoutAuthority() { - user.setAuthority(null); - when(apiKeyService.findApiKeyByValue(TEST_API_KEY)).thenReturn(apiKey); - when(userService.findUserById(tenantId, userId)).thenReturn(user); - when(userService.findUserCredentialsByUserId(tenantId, userId)).thenReturn(userCredentials); - ApiKeyAuthenticationToken token = new ApiKeyAuthenticationToken(new RawApiKeyToken(TEST_API_KEY)); - - provider.authenticate(token); + @Test + public void testUnauthorizedWhenKeyExpired() throws Exception { + ApiKeyInfo apiKeyInfo = constructApiKeyInfo(); + apiKeyInfo.setExpirationTime(System.currentTimeMillis() - 1000); + ApiKey savedApiKeyWithBad = doPost("/api/apiKey", apiKeyInfo, ApiKey.class); + setApiKey(savedApiKeyWithBad.getValue()); + doPost("/api/apiKey", savedApiKey, ApiKeyInfo.class); + doGetWithApiKey("/api/admin/featuresInfo").andExpect(status().isUnauthorized()); } - @Test - public void testSupports() { - assertTrue(provider.supports(ApiKeyAuthenticationToken.class)); + private ApiKeyInfo constructApiKeyInfo() { + ApiKeyInfo apiKeyInfo = new ApiKeyInfo(); + apiKeyInfo.setDescription("New API key description"); + apiKeyInfo.setEnabled(true); + apiKeyInfo.setUserId(tenantAdminUserId); + return apiKeyInfo; } } diff --git a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java index c016631064..f82be54c56 100644 --- a/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java +++ b/common/dao-api/src/main/java/org/thingsboard/server/dao/user/UserService.java @@ -17,6 +17,7 @@ package org.thingsboard.server.dao.user; import com.google.common.util.concurrent.ListenableFuture; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserAuthDetails; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.TenantProfileId; @@ -109,4 +110,6 @@ public interface UserService extends EntityDaoService { void removeMobileSession(TenantId tenantId, String mobileToken); + UserAuthDetails findUserAuthDetailsByUserId(TenantId tenantId, UserId userId); + } diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java index b55453f393..c97a3a9a21 100644 --- a/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java +++ b/common/data/src/main/java/org/thingsboard/server/common/data/CacheConstants.java @@ -40,6 +40,7 @@ public final class CacheConstants { public static final String SENT_NOTIFICATIONS_CACHE = "sentNotifications"; public static final String TRENDZ_SETTINGS_CACHE = "trendzSettings"; public static final String AI_MODEL_CACHE = "aiModel"; + public static final String API_KEYS_CACHE = "apiKeys"; public static final String ASSET_PROFILE_CACHE = "assetProfiles"; public static final String ATTRIBUTES_CACHE = "attributes"; diff --git a/common/data/src/main/java/org/thingsboard/server/common/data/UserAuthDetails.java b/common/data/src/main/java/org/thingsboard/server/common/data/UserAuthDetails.java new file mode 100644 index 0000000000..3bb05e8fee --- /dev/null +++ b/common/data/src/main/java/org/thingsboard/server/common/data/UserAuthDetails.java @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.common.data; + +public record UserAuthDetails(User user, boolean credentialsEnabled) {} diff --git a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java index d57301fd10..4350b2d66d 100644 --- a/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java +++ b/common/message/src/main/java/org/thingsboard/server/common/msg/plugin/ComponentLifecycleMsg.java @@ -30,9 +30,6 @@ import org.thingsboard.server.common.msg.cluster.ToAllNodesMsg; import java.io.Serial; import java.util.Optional; -/** - * @author Andrew Shvayka - */ @Data public class ComponentLifecycleMsg implements TenantAwareMsg, ToAllNodesMsg { diff --git a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java index c97fa00f80..5baa92f8c6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java +++ b/dao/src/main/java/org/thingsboard/server/dao/model/sql/UserEntity.java @@ -35,9 +35,6 @@ import org.thingsboard.server.dao.util.mapping.JsonConverter; import java.util.UUID; -/** - * Created by Valerii Sosliuk on 4/21/2017. - */ @Data @EqualsAndHashCode(callSuper = true) @Entity diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCacheKey.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCacheKey.java new file mode 100644 index 0000000000..a655ac1c25 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCacheKey.java @@ -0,0 +1,40 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.pat; + +import org.checkerframework.checker.nullness.qual.NonNull; + +import java.io.Serializable; + +import static java.util.Objects.requireNonNull; + +record ApiKeyCacheKey(String value) implements Serializable { + + ApiKeyCacheKey { + requireNonNull(value); + } + + static ApiKeyCacheKey of(String value) { + return new ApiKeyCacheKey(value); + } + + @NonNull + @Override + public String toString() { + return /* cache name */ "_" + value; + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCaffeineCache.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCaffeineCache.java new file mode 100644 index 0000000000..48eab7a32c --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyCaffeineCache.java @@ -0,0 +1,33 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.pat; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cache.CacheManager; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CaffeineTbTransactionalCache; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.pat.ApiKey; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "caffeine", matchIfMissing = true) +@Service("ApiKeyCache") +public class ApiKeyCaffeineCache extends CaffeineTbTransactionalCache { + + public ApiKeyCaffeineCache(CacheManager cacheManager) { + super(cacheManager, CacheConstants.API_KEYS_CACHE); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java index 73c57b2840..20448bb25c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyDao.java @@ -20,13 +20,15 @@ import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.dao.Dao; +import java.util.Set; + public interface ApiKeyDao extends Dao { ApiKey findByValue(String value); - void deleteByTenantId(TenantId tenantId); + Set deleteByTenantId(TenantId tenantId); - void deleteByUserId(TenantId tenantId, UserId userId); + Set deleteByUserId(TenantId tenantId, UserId userId); int deleteAllByExpirationTimeBefore(long ts); diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyEvictEvent.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyEvictEvent.java new file mode 100644 index 0000000000..d39149f11a --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyEvictEvent.java @@ -0,0 +1,18 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.pat; + +public record ApiKeyEvictEvent(String value) {} diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyRedisCache.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyRedisCache.java new file mode 100644 index 0000000000..eee0b8dc31 --- /dev/null +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyRedisCache.java @@ -0,0 +1,36 @@ +/** + * Copyright © 2016-2025 The Thingsboard Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.thingsboard.server.dao.pat; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.stereotype.Service; +import org.thingsboard.server.cache.CacheSpecsMap; +import org.thingsboard.server.cache.RedisTbTransactionalCache; +import org.thingsboard.server.cache.TBRedisCacheConfiguration; +import org.thingsboard.server.cache.TbJsonRedisSerializer; +import org.thingsboard.server.common.data.CacheConstants; +import org.thingsboard.server.common.data.pat.ApiKey; + +@ConditionalOnProperty(prefix = "cache", value = "type", havingValue = "redis") +@Service("ApiKeyCache") +public class ApiKeyRedisCache extends RedisTbTransactionalCache { + + public ApiKeyRedisCache(TBRedisCacheConfiguration configuration, CacheSpecsMap cacheSpecsMap, RedisConnectionFactory connectionFactory) { + super(CacheConstants.API_KEYS_CACHE, cacheSpecsMap, connectionFactory, configuration, new TbJsonRedisSerializer<>(ApiKey.class)); + } + +} diff --git a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java index cf4d4209af..682de95e78 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/pat/ApiKeyServiceImpl.java @@ -17,7 +17,10 @@ package org.thingsboard.server.dao.pat; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.id.ApiKeyId; @@ -29,10 +32,12 @@ import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.common.data.pat.ApiKeyInfo; -import org.thingsboard.server.dao.entity.AbstractEntityService; +import org.thingsboard.server.dao.entity.AbstractCachedEntityService; +import org.thingsboard.server.dao.eventsourcing.SaveEntityEvent; import org.thingsboard.server.dao.service.validator.ApiKeyDataValidator; import java.util.Optional; +import java.util.Set; import java.util.UUID; import static org.thingsboard.server.dao.service.Validator.validateId; @@ -42,15 +47,28 @@ import static org.thingsboard.server.dao.user.UserServiceImpl.INCORRECT_USER_ID; @Slf4j @Service @RequiredArgsConstructor -public class ApiKeyServiceImpl extends AbstractEntityService implements ApiKeyService { +public class ApiKeyServiceImpl extends AbstractCachedEntityService implements ApiKeyService { private static final String INCORRECT_API_KEY_ID = "Incorrect ApiKeyId "; - private static final int DEFAULT_API_KEY_BYTES = 32; + private static final int MAX_API_KEY_VALUE_LENGTH = 255; private final ApiKeyDao apiKeyDao; private final ApiKeyInfoDao apiKeyInfoDao; + @Lazy private final ApiKeyDataValidator apiKeyValidator; + @Value("${security.api_key.value_prefix:}") + private String prefix; + + @Value("${security.api_key.value_bytes_size:}") + private int valueBytesSize; + + @Override + @TransactionalEventListener + public void handleEvictEvent(ApiKeyEvictEvent event) { + cache.evict(ApiKeyCacheKey.of(event.value())); + } + @Override public ApiKey saveApiKey(TenantId tenantId, ApiKeyInfo apiKeyInfo) { log.trace("Executing saveApiKey [{}]", apiKeyInfo); @@ -58,14 +76,19 @@ public class ApiKeyServiceImpl extends AbstractEntityService implements ApiKeySe var apiKey = new ApiKey(apiKeyInfo); var old = apiKeyValidator.validate(apiKey, ApiKeyInfo::getTenantId); if (old == null) { - String hash = generateApiKeySecret(); - apiKey.setValue(hash); + String value = generateApiKeySecret(); + apiKey.setValue(value); } else { apiKey.setValue(old.getValue()); } - return apiKeyDao.save(tenantId, apiKey); + var savedApiKey = apiKeyDao.save(tenantId, apiKey); + eventPublisher.publishEvent(SaveEntityEvent.builder().tenantId(tenantId).entityId(savedApiKey.getId()).entity(savedApiKey).created(apiKey.getId() == null).build()); + if (old != null && old.isEnabled() != apiKey.isEnabled()) { + publishEvictEvent(new ApiKeyEvictEvent(apiKey.getValue())); + } + return savedApiKey; } catch (Exception e) { - checkConstraintViolation(e, "api_hash_unq_key", "Api Key with such hash already exists!"); + checkConstraintViolation(e, "api_key_value_unq_key", "Api Key with such value already exists!"); throw e; } } @@ -91,46 +114,37 @@ public class ApiKeyServiceImpl extends AbstractEntityService implements ApiKeySe @Override public void deleteApiKey(TenantId tenantId, ApiKey apiKey, boolean force) { - deleteApiKey(tenantId, apiKey.getId()); - } - - @Override - public void deleteEntity(TenantId tenantId, EntityId id, boolean force) { - deleteApiKey(tenantId, id); - } - - private void deleteApiKey(TenantId tenantId, EntityId entityId) { - UUID apiKeyId = entityId.getId(); + UUID apiKeyId = apiKey.getUuidId(); validateId(apiKeyId, id -> INCORRECT_API_KEY_ID + id); - ApiKey apiKey = apiKeyDao.findById(tenantId, apiKeyId); - if (apiKey == null) { - return; - } apiKeyDao.removeById(tenantId, apiKeyId); + publishEvictEvent(new ApiKeyEvictEvent(apiKey.getValue())); } @Override public void deleteByTenantId(TenantId tenantId) { log.trace("Executing deleteApiKeysByTenantId, tenantId [{}]", tenantId); validateId(tenantId, id -> INCORRECT_TENANT_ID + id); - apiKeyDao.deleteByTenantId(tenantId); + Set values = apiKeyDao.deleteByTenantId(tenantId); + values.forEach(value -> publishEvictEvent(new ApiKeyEvictEvent(value))); } @Override public void deleteByUserId(TenantId tenantId, UserId userId) { log.trace("Executing deleteApiKeysByUserId, tenantId [{}]", tenantId); validateId(userId, id -> INCORRECT_USER_ID + id); - apiKeyDao.deleteByUserId(tenantId, userId); + Set values = apiKeyDao.deleteByUserId(tenantId, userId); + values.forEach(value -> publishEvictEvent(new ApiKeyEvictEvent(value))); } @Override public ApiKey findApiKeyByValue(String value) { log.trace("Executing findApiKeyByValue [{}]", value); - return apiKeyDao.findByValue(value); + var cacheKey = ApiKeyCacheKey.of(value); + return cache.getAndPutInTransaction(cacheKey, () -> apiKeyDao.findByValue(value), true); } - private static String generateApiKeySecret() { - return StringUtils.generateSafeToken(DEFAULT_API_KEY_BYTES); + private String generateApiKeySecret() { + return prefix + StringUtils.generateSafeToken(Math.min(valueBytesSize, MAX_API_KEY_VALUE_LENGTH)); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java index a1431846f7..e539c1e721 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/ApiKeyDataValidator.java @@ -22,22 +22,22 @@ import org.thingsboard.server.common.data.pat.ApiKey; import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.dao.pat.ApiKeyDao; import org.thingsboard.server.dao.service.DataValidator; -import org.thingsboard.server.dao.tenant.TenantDao; -import org.thingsboard.server.dao.user.UserDao; +import org.thingsboard.server.dao.tenant.TenantService; +import org.thingsboard.server.dao.user.UserService; @Component @RequiredArgsConstructor public class ApiKeyDataValidator extends DataValidator { private final ApiKeyDao apiKeyDao; - private final TenantDao tenantDao; - private final UserDao userDao; + private final TenantService tenantService; + private final UserService userService; @Override protected void validateDataImpl(TenantId tenantId, ApiKey apiKey) { if (apiKey.getId() != null) { if (apiKey.getUuidId() == null) { - throw new DataValidationException("Api Key UUID should be specified!"); + throw new DataValidationException("API Key UUID should be specified!"); } if (apiKey.getId().isNullUid()) { throw new DataValidationException("API key UUID must not be the reserved null value!"); @@ -47,14 +47,14 @@ public class ApiKeyDataValidator extends DataValidator { if (apiKey.getTenantId() == null || apiKey.getTenantId().getId() == null) { throw new DataValidationException("API key should be assigned to tenant!"); } - if (!TenantId.SYS_TENANT_ID.equals(apiKey.getTenantId()) && tenantDao.findById(apiKey.getTenantId(), apiKey.getTenantId().getId()) == null) { + if (!TenantId.SYS_TENANT_ID.equals(apiKey.getTenantId()) && !tenantService.tenantExists(apiKey.getTenantId())) { throw new DataValidationException("API key reference a non-existent tenant!"); } if (apiKey.getUserId() == null || apiKey.getUserId().getId() == null) { throw new DataValidationException("API key should be assigned to user!"); } - if (userDao.findById(apiKey.getTenantId(), apiKey.getUserId().getId()) == null) { + if (userService.findUserById(apiKey.getTenantId(), apiKey.getUserId()) == null) { throw new DataValidationException("API key reference a non-existent user!"); } } @@ -66,10 +66,10 @@ public class ApiKeyDataValidator extends DataValidator { throw new DataValidationException("Cannot update non-existent API key!"); } if (!old.getUserId().equals(apiKey.getUserId())) { - throw new DataValidationException("Cannot update api key user id!"); + throw new DataValidationException("Cannot update API key user id!"); } if (old.getExpirationTime() != apiKey.getExpirationTime()) { - throw new DataValidationException("Cannot update api key expiration time!"); + throw new DataValidationException("Cannot update API key expiration time!"); } return old; } diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java index 2c797db7c1..8b96776d4d 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/ApiKeyRepository.java @@ -22,6 +22,7 @@ import org.springframework.data.repository.query.Param; import org.springframework.transaction.annotation.Transactional; import org.thingsboard.server.dao.model.sql.ApiKeyEntity; +import java.util.Set; import java.util.UUID; public interface ApiKeyRepository extends JpaRepository { @@ -30,14 +31,24 @@ public interface ApiKeyRepository extends JpaRepository { @Transactional @Modifying - @Query("DELETE FROM ApiKeyEntity ak WHERE ak.tenantId = :tenantId") - void deleteByTenantId(@Param("tenantId") UUID tenantId); + @Query(value = """ + DELETE FROM api_key + WHERE tenant_id = :tenantId + RETURNING value + """, nativeQuery = true + ) + Set deleteByTenantId(@Param("tenantId") UUID tenantId); @Transactional @Modifying - @Query("DELETE FROM ApiKeyEntity ak WHERE ak.tenantId = :tenantId AND ak.userId = :userId") - void deleteByUserId(@Param("tenantId") UUID tenantId, - @Param("userId") UUID userId); + @Query(value = """ + DELETE FROM api_key + WHERE tenant_id = :tenantId AND user_id = :userId + RETURNING value + """, nativeQuery = true + ) + Set deleteByUserId(@Param("tenantId") UUID tenantId, + @Param("userId") UUID userId); @Transactional @Modifying diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java index f539170798..76bd7e52b6 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/pat/JpaApiKeyDao.java @@ -46,13 +46,13 @@ public class JpaApiKeyDao extends JpaAbstractDao implement } @Override - public void deleteByTenantId(TenantId tenantId) { - apiKeyRepository.deleteByTenantId(tenantId.getId()); + public Set deleteByTenantId(TenantId tenantId) { + return apiKeyRepository.deleteByTenantId(tenantId.getId()); } @Override - public void deleteByUserId(TenantId tenantId, UserId userId) { - apiKeyRepository.deleteByUserId(tenantId.getId(), userId.getId()); + public Set deleteByUserId(TenantId tenantId, UserId userId) { + return apiKeyRepository.deleteByUserId(tenantId.getId(), userId.getId()); } @Override diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java index 35d15bab51..95a5604bfb 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/JpaUserDao.java @@ -21,6 +21,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Component; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserAuthDetails; import org.thingsboard.server.common.data.edqs.fields.UserFields; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; @@ -28,6 +29,7 @@ import org.thingsboard.server.common.data.id.TenantProfileId; import org.thingsboard.server.common.data.page.PageData; import org.thingsboard.server.common.data.page.PageLink; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.DaoUtil; import org.thingsboard.server.dao.model.sql.UserEntity; import org.thingsboard.server.dao.sql.JpaAbstractDao; @@ -136,6 +138,12 @@ public class JpaUserDao extends JpaAbstractDao implements User DaoUtil.toPageable(pageLink))); } + @Override + public UserAuthDetails findUserAuthDetailsByUserId(UUID tenantId, UUID userId) { + TbPair result = userRepository.findUserAuthDetailsByUserId(userId); + return new UserAuthDetails(result.getFirst().toData(), result.getSecond()); + } + @Override public Long countByTenantId(TenantId tenantId) { return userRepository.countByTenantId(tenantId.getId()); diff --git a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java index 0a30a859c6..5a806592de 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java +++ b/dao/src/main/java/org/thingsboard/server/dao/sql/user/UserRepository.java @@ -23,15 +23,13 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; import org.thingsboard.server.common.data.edqs.fields.UserFields; import org.thingsboard.server.common.data.security.Authority; +import org.thingsboard.server.common.data.util.TbPair; import org.thingsboard.server.dao.model.sql.UserEntity; import java.util.Collection; import java.util.List; import java.util.UUID; -/** - * @author Valerii Sosliuk - */ public interface UserRepository extends JpaRepository { UserEntity findByEmail(String email); @@ -78,4 +76,9 @@ public interface UserRepository extends JpaRepository { "u.customerId, u.version, u.firstName, u.lastName, u.email, u.phone, u.additionalInfo) " + "FROM UserEntity u WHERE u.id > :id ORDER BY u.id") List findNextBatch(@Param("id") UUID id, Limit limit); + + @Query("SELECT new org.thingsboard.server.common.data.util.TbPair(u, uc.enabled) " + + "FROM UserEntity u JOIN UserCredentialsEntity uc ON u.id = uc.userId WHERE u.id = :userId ") + TbPair findUserAuthDetailsByUserId(@Param("userId") UUID userId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java index 0df7c36527..fa8b133765 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/tenant/TenantServiceImpl.java @@ -171,7 +171,7 @@ public class TenantServiceImpl extends AbstractCachedEntityService, TenantEntityDao { PageData findByAuthorityAndTenantProfilesIds(Authority authority, List tenantProfilesIds, PageLink pageLink); + UserAuthDetails findUserAuthDetailsByUserId(UUID tenantId, UUID userId); + } diff --git a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java index 88973b3f6c..04f5f0a46c 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java +++ b/dao/src/main/java/org/thingsboard/server/dao/user/UserServiceImpl.java @@ -34,6 +34,7 @@ import org.thingsboard.server.cache.user.UserCacheKey; import org.thingsboard.server.common.data.EntityType; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.UserAuthDetails; import org.thingsboard.server.common.data.audit.ActionType; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.EntityId; @@ -296,7 +297,7 @@ public class UserServiceImpl extends AbstractCachedEntityService INCORRECT_USER_ID + id); + return userDao.findUserAuthDetailsByUserId(tenantId.getId(), userId.getId()); + } + private Optional findMobileSessionInfo(TenantId tenantId, UserId userId) { return Optional.ofNullable(userSettingsService.findUserSettings(tenantId, userId, UserSettingsType.MOBILE)) .map(UserSettings::getSettings).map(settings -> JacksonUtil.treeToValue(settings, UserMobileSessionInfo.class)); diff --git a/dao/src/main/resources/sql/schema-entities.sql b/dao/src/main/resources/sql/schema-entities.sql index 9d811250ac..b5bd181f54 100644 --- a/dao/src/main/resources/sql/schema-entities.sql +++ b/dao/src/main/resources/sql/schema-entities.sql @@ -714,11 +714,11 @@ CREATE TABLE IF NOT EXISTS api_key ( created_time bigint NOT NULL, tenant_id uuid, user_id uuid, - value varchar(255), + value varchar(512), enabled boolean NOT NULL DEFAULT TRUE, expiration_time bigint DEFAULT 0, description varchar(1024), - CONSTRAINT api_value_unq_key UNIQUE (value) + CONSTRAINT api_key_value_unq_key UNIQUE (value) ); CREATE TABLE IF NOT EXISTS resource ( diff --git a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java index fcc83e1149..f6f91ca0b2 100644 --- a/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java +++ b/rest-client/src/main/java/org/thingsboard/rest/client/RestClient.java @@ -226,7 +226,7 @@ public class RestClient implements Closeable { private long refreshTokenExpTs; private long clientServerTimeDiff; - public enum AuthType { JWT, API_KEY } + public enum AuthType {JWT, API_KEY} public RestClient(String baseURL) { this(new RestTemplate(), baseURL); @@ -236,6 +236,10 @@ public class RestClient implements Closeable { this(restTemplate, baseURL, AuthType.JWT, null); } + public RestClient(RestTemplate restTemplate, String baseURL, String accessToken) { + this(restTemplate, baseURL, AuthType.JWT, accessToken); + } + public RestClient(RestTemplate restTemplate, String baseURL, AuthType authType, String token) { this.restTemplate = restTemplate; this.loginRestTemplate = new RestTemplate(restTemplate.getRequestFactory()); @@ -3012,7 +3016,7 @@ public class RestClient implements Closeable { addWidgetInfoFiltersToParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList, params); return restTemplate.exchange( baseURL + "/api/widgetTypes?" + getUrlParams(pageLink) + - getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), + getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() { @@ -3100,7 +3104,7 @@ public class RestClient implements Closeable { addWidgetInfoFiltersToParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList, params); return restTemplate.exchange( baseURL + "/api/widgetTypesInfos?widgetsBundleId={widgetsBundleId}&" + getUrlParams(pageLink) + - getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), + getWidgetTypeInfoPageRequestUrlParams(tenantOnly, fullSearch, deprecatedFilter, widgetTypeList), HttpMethod.GET, HttpEntity.EMPTY, new ParameterizedTypeReference>() {