From fcc2a917fde477467e54e0ecb4ea74ed016479a6 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Tue, 23 Sep 2025 12:00:16 +0300 Subject: [PATCH 01/11] Added User sync for Edge --- .../service/edge/EdgeContextComponent.java | 4 + .../service/edge/rpc/EdgeGrpcSession.java | 24 +++ .../rpc/processor/user/BaseUserProcessor.java | 130 ++++++++++++ .../rpc/processor/user/UserEdgeProcessor.java | 97 ++++++++- .../rpc/processor/user/UserProcessor.java | 31 +++ .../thingsboard/server/edge/UserEdgeTest.java | 193 ++++++++++++------ .../server/dao/user/UserService.java | 7 + common/edge-api/src/main/proto/edge.proto | 2 + .../service/validator/UserDataValidator.java | 1 + .../server/dao/user/UserServiceImpl.java | 77 +++++-- 10 files changed, 485 insertions(+), 81 deletions(-) create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java create mode 100644 application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java diff --git a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java index 5c5eea32fd..0f469b15b8 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/EdgeContextComponent.java @@ -73,6 +73,7 @@ import org.thingsboard.server.service.edge.rpc.processor.resource.ResourceEdgePr import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.rule.RuleChainMetadataEdgeProcessor; import org.thingsboard.server.service.edge.rpc.processor.telemetry.TelemetryEdgeProcessor; +import org.thingsboard.server.service.edge.rpc.processor.user.UserProcessor; import org.thingsboard.server.service.edge.rpc.sync.EdgeRequestsService; import org.thingsboard.server.service.executors.GrpcCallbackExecutorService; @@ -261,6 +262,9 @@ public class EdgeContextComponent { @Autowired private CalculatedFieldProcessor calculatedFieldProcessor; + @Autowired + private UserProcessor userProcessor; + public EdgeProcessor getProcessor(EdgeEventType edgeEventType) { EdgeProcessor processor = processorMap.get(edgeEventType); if (processor == null) { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java index a86409c6af..393d1ba3da 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/EdgeGrpcSession.java @@ -83,6 +83,8 @@ import org.thingsboard.server.gen.edge.v1.SyncCompletedMsg; import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UplinkResponseMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.gen.edge.v1.WidgetBundleTypesRequestMsg; import org.thingsboard.server.service.edge.EdgeContextComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; @@ -100,6 +102,7 @@ import java.util.UUID; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; @@ -121,6 +124,7 @@ public abstract class EdgeGrpcSession implements Closeable { private final EdgeSessionState sessionState = new EdgeSessionState(); private final ReentrantLock downlinkMsgLock = new ReentrantLock(); + private final Lock sequenceDependencyLock = new ReentrantLock(); protected EdgeContextComponent ctx; protected Edge edge; @@ -934,6 +938,26 @@ public abstract class EdgeGrpcSession implements Closeable { result.add(ctx.getCalculatedFieldProcessor().processCalculatedFieldMsgFromEdge(edge.getTenantId(), edge, calculatedFieldUpdateMsg)); } } + if (uplinkMsg.getUserUpdateMsgCount() > 0) { + for (UserUpdateMsg userUpdateMsg : uplinkMsg.getUserUpdateMsgList()) { + sequenceDependencyLock.lock(); + try { + result.add(ctx.getUserProcessor().processUserMsgFromEdge(edge.getTenantId(), edge, userUpdateMsg)); + } finally { + sequenceDependencyLock.unlock(); + } + } + } + if (uplinkMsg.getUserCredentialsUpdateMsgCount() > 0) { + for (UserCredentialsUpdateMsg userCredentialsUpdateMsg : uplinkMsg.getUserCredentialsUpdateMsgList()) { + sequenceDependencyLock.lock(); + try { + result.add(ctx.getUserProcessor().processUserCredentialsMsgFromEdge(edge.getTenantId(), edge, userCredentialsUpdateMsg)); + } finally { + sequenceDependencyLock.unlock(); + } + } + } } catch (Exception e) { String failureMsg = String.format("Can't process uplink msg [%s] from edge", uplinkMsg); log.trace("[{}][{}] Can't process uplink msg [{}]", tenantId, edge.getId(), uplinkMsg, e); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java new file mode 100644 index 0000000000..92d5664a4f --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -0,0 +1,130 @@ +/** + * 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.edge.rpc.processor.user; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.util.Pair; +import org.thingsboard.common.util.JacksonUtil; +import org.thingsboard.server.common.data.StringUtils; +import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.dao.service.DataValidator; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +@Slf4j +public abstract class BaseUserProcessor extends BaseEdgeProcessor { + + @Autowired + private DataValidator userValidator; + + protected Pair saveOrUpdateUser(TenantId tenantId, UserId userId, UserUpdateMsg userUpdateMsg) { + boolean isCreated = false; + boolean userEmailUpdated = false; + + try { + User user = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); + if (user == null) { + throw new RuntimeException("[{" + tenantId + "}] userUpdateMsg {" + userUpdateMsg + "} cannot be converted to user"); + } + + User userById = edgeCtx.getUserService().findUserById(tenantId, userId); + if (userById == null) { + isCreated = true; + user.setId(null); + } else { + user.setId(userId); + } + + String userEmail = user.getEmail(); + User existing = edgeCtx.getUserService().findUserByTenantIdAndEmail(tenantId, user.getEmail()); + + if (existing != null && !existing.getId().equals(user.getId())) { + String[] splitEmail = userEmail.split("@"); + userEmail = splitEmail[0] + "_" + StringUtils.randomAlphanumeric(15) + "@" + splitEmail[1]; + log.warn("[{}] User with email {} already exists. Renaming User email to {}", + tenantId, user.getEmail(), userEmail); + userEmailUpdated = true; + } + user.setEmail(userEmail); + setCustomerId(tenantId, isCreated ? null : userById.getCustomerId(), user, userUpdateMsg); + + userValidator.validate(user, User::getTenantId); + + if (isCreated) { + user.setId(userId); + } + + edgeCtx.getUserService().saveUser(tenantId, user, false); + } catch (Exception e) { + log.error("[{}] Failed to process user update msg [{}]", tenantId, userUpdateMsg, e); + throw e; + } + + return Pair.of(isCreated, userEmailUpdated); + } + + protected void updateUserCredentials(TenantId tenantId, UserCredentialsUpdateMsg updateMsg) { + UserCredentials userCredentialsFromUpdateMsg = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true); + if (userCredentialsFromUpdateMsg == null) { + throw new RuntimeException(String.format("[%s] Failed to parse UserCredentials from updateMsg: %s", tenantId, updateMsg)); + } + + User user = edgeCtx.getUserService().findUserById(tenantId, userCredentialsFromUpdateMsg.getUserId()); + if (user == null) { + log.warn("[{}] Can't find user by id [{}] skipping credentials update. UserCredentialsUpdateMsg [{}]", + tenantId, userCredentialsFromUpdateMsg.getUserId(), updateMsg); + return; + } + + log.debug("[{}] Updating user credentials for user [{}]. New credentials Id [{}], enabled [{}]", + tenantId, user.getName(), userCredentialsFromUpdateMsg.getId(), userCredentialsFromUpdateMsg.isEnabled()); + + try { + UserCredentials existing = edgeCtx.getUserService().findUserCredentialsByUserId(tenantId, user.getId()); + boolean created = existing == null; + + UserCredentials updated = created ? new UserCredentials() : existing; + updated.setId(userCredentialsFromUpdateMsg.getId()); + updated.setUserId(user.getId()); + updated.setEnabled(userCredentialsFromUpdateMsg.isEnabled()); + updated.setActivateToken(userCredentialsFromUpdateMsg.getActivateToken()); + updated.setAdditionalInfo(userCredentialsFromUpdateMsg.getAdditionalInfo()); + updated.setPassword(userCredentialsFromUpdateMsg.getPassword()); + updated.setResetToken(userCredentialsFromUpdateMsg.getResetToken()); + + + if (created) { + edgeCtx.getUserService().saveUserCredentials(tenantId, updated, false); + } else { + edgeCtx.getUserService().replaceUserCredentials(tenantId, updated, existing.getId(), false); + } + } catch (Exception e) { + log.error("[{}] Can't update user credentials for user [{}], userCredentialsUpdateMsg [{}]", + tenantId, user.getName(), updateMsg, e); + throw new RuntimeException(e); + } + + } + + protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg); + +} diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index fdd03d63f3..e0bd07c6c6 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -15,26 +15,106 @@ */ package org.thingsboard.server.service.edge.rpc.processor.user; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import lombok.extern.slf4j.Slf4j; +import org.springframework.data.util.Pair; import org.springframework.stereotype.Component; +import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; import org.thingsboard.server.common.data.edge.EdgeEvent; +import org.thingsboard.server.common.data.edge.EdgeEventActionType; import org.thingsboard.server.common.data.edge.EdgeEventType; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.msg.TbMsgMetaData; +import org.thingsboard.server.dao.exception.DataValidationException; import org.thingsboard.server.gen.edge.v1.DownlinkMsg; import org.thingsboard.server.gen.edge.v1.EdgeVersion; import org.thingsboard.server.gen.edge.v1.UpdateMsgType; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; import org.thingsboard.server.queue.util.TbCoreComponent; import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; -import org.thingsboard.server.service.edge.rpc.processor.BaseEdgeProcessor; + +import java.util.UUID; @Slf4j @Component @TbCoreComponent -public class UserEdgeProcessor extends BaseEdgeProcessor { +public class UserEdgeProcessor extends BaseUserProcessor implements UserProcessor { + + @Override + public ListenableFuture processUserMsgFromEdge(TenantId tenantId, Edge edge, UserUpdateMsg userUpdateMsg) { + log.trace("[{}] executing processUserMsgFromEdge [{}] from edge [{}]", tenantId, userUpdateMsg, edge.getId()); + UserId userId = new UserId(new UUID(userUpdateMsg.getIdMSB(), userUpdateMsg.getIdLSB())); + try { + edgeSynchronizationManager.getEdgeId().set(edge.getId()); + + return switch (userUpdateMsg.getMsgType()) { + case ENTITY_CREATED_RPC_MESSAGE, ENTITY_UPDATED_RPC_MESSAGE -> { + saveOrUpdateUser(tenantId, userId, userUpdateMsg, edge); + yield Futures.immediateFuture(null); + } + default -> handleUnsupportedMsgType(userUpdateMsg.getMsgType()); + }; + } catch (DataValidationException e) { + if (e.getMessage().contains("limit reached")) { + log.warn("[{}] Number of allowed users violated {}", tenantId, userUpdateMsg, e); + return Futures.immediateFuture(null); + } else { + return Futures.immediateFailedFuture(e); + } + } finally { + edgeSynchronizationManager.getEdgeId().remove(); + } + } + + @Override + public ListenableFuture processUserCredentialsMsgFromEdge(TenantId tenantId, Edge edge, UserCredentialsUpdateMsg userCredentialsUpdateMsg) { + log.debug("[{}] Executing processUserCredentialsMsgFromEdge, userCredentialsUpdateMsg [{}]", tenantId, userCredentialsUpdateMsg); + try { + edgeSynchronizationManager.getEdgeId().set(edge.getId()); + + super.updateUserCredentials(tenantId, userCredentialsUpdateMsg); + } finally { + edgeSynchronizationManager.getEdgeId().remove(); + } + return Futures.immediateFuture(null); + } + + private void saveOrUpdateUser(TenantId tenantId, UserId userId, UserUpdateMsg userUpdateMsg, Edge edge) { + Pair resultPair = super.saveOrUpdateUser(tenantId, userId, userUpdateMsg); + boolean isCreated = resultPair.getFirst(); + if (isCreated) { + createRelationFromEdge(tenantId, edge.getId(), userId); + pushUserCreatedEventToRuleEngine(tenantId, edge, userId); + } + + Boolean userEmailUpdated = resultPair.getSecond(); + + if (userEmailUpdated) { + saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.USER, EdgeEventActionType.UPDATED, userId, null); + } + } + + private void pushUserCreatedEventToRuleEngine(TenantId tenantId, Edge edge, UserId userId) { + try { + User user = edgeCtx.getUserService().findUserById(tenantId, userId); + if (user != null) { + String userAsString = JacksonUtil.toString(user); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, user.getCustomerId()); + pushEntityEventToRuleEngine(tenantId, userId, user.getCustomerId(), TbMsgType.ENTITY_CREATED, userAsString, msgMetaData); + } + } catch (Exception e) { + log.warn("[{}][{}] Failed to push user action to rule engine: {}", tenantId, userId, TbMsgType.ENTITY_CREATED.name(), e); + } + } @Override public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { @@ -48,7 +128,7 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) .addUserUpdateMsg(EdgeMsgConstructorUtils.constructUserUpdatedMsg(msgType, user)); UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); - if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { + if (userCredentialsByUserId != null) { builder.addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId)); } return builder.build(); @@ -62,11 +142,10 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { } case CREDENTIALS_UPDATED -> { UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(edgeEvent.getTenantId(), userId); - if (userCredentialsByUserId != null && userCredentialsByUserId.isEnabled()) { - UserCredentialsUpdateMsg userCredentialsUpdateMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId); + if (userCredentialsByUserId != null) { return DownlinkMsg.newBuilder() .setDownlinkMsgId(EdgeUtils.nextPositiveInt()) - .addUserCredentialsUpdateMsg(userCredentialsUpdateMsg) + .addUserCredentialsUpdateMsg(EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentialsByUserId)) .build(); } } @@ -79,4 +158,10 @@ public class UserEdgeProcessor extends BaseEdgeProcessor { return EdgeEventType.USER; } + @Override + protected void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg) { + CustomerId customerUUID = user.getCustomerId() != null ? user.getCustomerId() : customerId; + user.setCustomerId(customerUUID); + } + } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java new file mode 100644 index 0000000000..dd9659c1f4 --- /dev/null +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserProcessor.java @@ -0,0 +1,31 @@ +/** + * 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.edge.rpc.processor.user; + +import com.google.common.util.concurrent.ListenableFuture; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; +import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.service.edge.rpc.processor.EdgeProcessor; + +public interface UserProcessor extends EdgeProcessor { + + ListenableFuture processUserMsgFromEdge(TenantId tenantId, Edge edge, UserUpdateMsg userUpdateMsg); + + ListenableFuture processUserCredentialsMsgFromEdge(TenantId tenantId, Edge edge, UserCredentialsUpdateMsg userCredentialsUpdateMsg); + +} diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 05debdc015..84442e2d0a 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -15,6 +15,7 @@ */ package org.thingsboard.server.edge; +import com.fasterxml.jackson.databind.JsonNode; import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; @@ -22,8 +23,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.id.CustomerId; +import org.thingsboard.server.common.data.id.UserCredentialsId; +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.service.DaoSqlTest; @@ -32,11 +37,14 @@ import org.thingsboard.server.gen.edge.v1.UplinkMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsRequestMsg; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; +import org.thingsboard.server.service.edge.EdgeMsgConstructorUtils; import org.thingsboard.server.service.security.model.ChangePasswordRequest; import java.util.Optional; +import java.util.UUID; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.thingsboard.server.dao.user.UserServiceImpl.DEFAULT_TOKEN_LENGTH; @DaoSqlTest public class UserEdgeTest extends AbstractEdgeTest { @@ -44,46 +52,35 @@ public class UserEdgeTest extends AbstractEdgeTest { @Autowired private BCryptPasswordEncoder passwordEncoder; + private static final String DEFAULT_FIRST_NAME = "Boris"; + private static final String UPDATED_LAST_NAME = "Borisov"; + @Test public void testCreateUpdateDeleteTenantUser() throws Exception { // create user edgeImitator.expectMessageAmount(3); - User newTenantAdmin = new User(); - newTenantAdmin.setAuthority(Authority.TENANT_ADMIN); - newTenantAdmin.setTenantId(tenantId); - newTenantAdmin.setEmail("tenantAdmin@thingsboard.org"); - newTenantAdmin.setFirstName("Boris"); - newTenantAdmin.setLastName("Johnson"); + User newTenantAdmin = buildUser(Authority.TENANT_ADMIN, null, "tenantAdmin@thingsboard.org", DEFAULT_FIRST_NAME, "Johnson"); User savedTenantAdmin = createUser(newTenantAdmin, "tenant"); Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); - Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); - Assert.assertTrue(userUpdateMsgOpt.isPresent()); - UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); + + UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); Assert.assertNotNull(userMsg); Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenantAdmin.getId(), userMsg.getId()); - Assert.assertEquals(savedTenantAdmin.getAuthority(), userMsg.getAuthority()); - Assert.assertEquals(savedTenantAdmin.getEmail(), userMsg.getEmail()); - Assert.assertEquals(savedTenantAdmin.getFirstName(), userMsg.getFirstName()); - Assert.assertEquals(savedTenantAdmin.getLastName(), userMsg.getLastName()); - Optional userCredentialsUpdateMsgOpt = edgeImitator.findMessageByType(UserCredentialsUpdateMsg.class); - Assert.assertTrue(userCredentialsUpdateMsgOpt.isPresent()); // update user edgeImitator.expectMessageAmount(2); - savedTenantAdmin.setLastName("Borisov"); + savedTenantAdmin.setLastName(UPDATED_LAST_NAME); savedTenantAdmin = doPost("/api/user", savedTenantAdmin, User.class); Assert.assertTrue(edgeImitator.waitForMessages()); - userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); - Assert.assertTrue(userUpdateMsgOpt.isPresent()); - userUpdateMsg = userUpdateMsgOpt.get(); - userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); - Assert.assertNotNull(userMsg); + + userUpdateMsg = getLatestUserUpdateMsg(); + User userFromMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); + Assert.assertNotNull(userFromMsg); Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenantAdmin.getLastName(), userMsg.getLastName()); + Assert.assertEquals(UPDATED_LAST_NAME, userFromMsg.getLastName()); // update user credentials login(savedTenantAdmin.getEmail(), "tenant"); @@ -94,6 +91,7 @@ public class UserEdgeTest extends AbstractEdgeTest { changePasswordRequest.setNewPassword("newTenant"); doPost("/api/auth/changePassword", changePasswordRequest); Assert.assertTrue(edgeImitator.waitForMessages()); + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; @@ -109,6 +107,7 @@ public class UserEdgeTest extends AbstractEdgeTest { doDelete("/api/user/" + savedTenantAdmin.getUuidId()) .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); + latestMessage = edgeImitator.getLatestMessage(); Assert.assertTrue(latestMessage instanceof UserUpdateMsg); userUpdateMsg = (UserUpdateMsg) latestMessage; @@ -120,27 +119,11 @@ public class UserEdgeTest extends AbstractEdgeTest { @Test public void testCreateUpdateDeleteCustomerUser() throws Exception { // create customer - edgeImitator.expectMessageAmount(1); - Customer customer = new Customer(); - customer.setTitle("Edge Customer"); - Customer savedCustomer = doPost("/api/customer", customer, Customer.class); - Assert.assertFalse(edgeImitator.waitForMessages(5)); - - // assign edge to customer - edgeImitator.expectMessageAmount(2); - doPost("/api/customer/" + savedCustomer.getUuidId() - + "/edge/" + edge.getUuidId(), Edge.class); - Assert.assertTrue(edgeImitator.waitForMessages()); + Customer savedCustomer = createAndAssignCustomerToEdge("Edge Customer"); // create user edgeImitator.expectMessageAmount(3); - User customerUser = new User(); - customerUser.setAuthority(Authority.CUSTOMER_USER); - customerUser.setTenantId(tenantId); - customerUser.setCustomerId(savedCustomer.getId()); - customerUser.setEmail("customerUser@thingsboard.org"); - customerUser.setFirstName("John"); - customerUser.setLastName("Edwards"); + User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", "John", "Edwards"); User savedCustomerUser = createUser(customerUser, "customer"); Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); @@ -205,32 +188,57 @@ public class UserEdgeTest extends AbstractEdgeTest { } @Test - public void testSendUserCredentialsRequestToCloud() throws Exception { - UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); - UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); - userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits()); - userCredentialsRequestMsgBuilder.setUserIdLSB(tenantAdminUserId.getId().getLeastSignificantBits()); - testAutoGeneratedCodeByProtobuf(userCredentialsRequestMsgBuilder); - uplinkMsgBuilder.addUserCredentialsRequestMsg(userCredentialsRequestMsgBuilder.build()); + public void testSendUserToCloudFromEdge() throws Exception { + // create customer + Customer savedCustomer = createAndAssignCustomerToEdge("Edge Customer"); - testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + // create user + User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", DEFAULT_FIRST_NAME, "Johnson"); + + UUID uuid = UUID.randomUUID(); + customerUser.setId(new UserId(uuid)); + UUID userCredentialsUuid = UUID.randomUUID(); + UplinkMsg uplinkMsg = constructUserUplinkMsg(customerUser, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userCredentialsUuid); edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.sendUplinkMsg(uplinkMsg); Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); - UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); - Assert.assertNotNull(userCredentialsMsg); - Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId()); + User userFromCloud = doGet("/api/user/" + uuid, User.class); + Assert.assertNotNull(userFromCloud); + Assert.assertEquals(customerUser.getEmail(), userFromCloud.getEmail()); + //check user with existing email + User userWithExistingEmail = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", DEFAULT_FIRST_NAME, "Johnson"); + + UUID uuidForExistingEmail = UUID.randomUUID(); + userWithExistingEmail.setId(new UserId(uuidForExistingEmail)); + UUID userCredentialsUuidForExistingEmail = UUID.randomUUID(); + UplinkMsg uplinkMsgForExistingEmail = constructUserUplinkMsg(userWithExistingEmail, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userCredentialsUuidForExistingEmail); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgForExistingEmail); + Assert.assertTrue(edgeImitator.waitForResponses()); + + User userFromCloudWithExistingEmail = doGet("/api/user/" + uuidForExistingEmail, User.class); + Assert.assertNotNull(userFromCloudWithExistingEmail); + Assert.assertNotEquals(userWithExistingEmail.getEmail(), userFromCloudWithExistingEmail.getEmail()); + + assertUserCredentialsFlags(userFromCloud, false, false); + + UplinkMsg enabledCredentialsUplinkMsg = constructUserCredentialsUplinkMsg(customerUser.getId(), "password", true, userCredentialsUuid); + + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(enabledCredentialsUplinkMsg); + Assert.assertTrue(edgeImitator.waitForResponses()); + + User cloudUserWithCredentials = doGet("/api/user/" + uuid, User.class); + Assert.assertNotNull(cloudUserWithCredentials); + + assertUserCredentialsFlags(cloudUserWithCredentials, true, true); } @Test - public void sendUserCredentialsRequest() throws Exception { + public void testSendUserCredentialsRequest() throws Exception { UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits()); @@ -256,4 +264,71 @@ public class UserEdgeTest extends AbstractEdgeTest { testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); } + private User buildUser(Authority authority, CustomerId customerId, String email, String firstName, String lastName) { + User customerUser = new User(); + customerUser.setAuthority(authority); + customerUser.setTenantId(tenantId); + customerUser.setCustomerId(customerId); + customerUser.setEmail(email); + customerUser.setFirstName(firstName); + customerUser.setLastName(lastName); + return customerUser; + } + + private Customer createAndAssignCustomerToEdge(String title) throws Exception { + edgeImitator.expectMessageAmount(1); + Customer customer = new Customer(); + customer.setTitle(title); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + Assert.assertFalse(edgeImitator.waitForMessages(5)); + + edgeImitator.expectMessageAmount(2); + doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + return savedCustomer; + } + + private UplinkMsg constructUserUplinkMsg(User user, UpdateMsgType msgType, UUID userCredentialsUuid) { + UserUpdateMsg userUpdateMsg = EdgeMsgConstructorUtils.constructUserUpdatedMsg(msgType, user); + + UserCredentials userCredentials = new UserCredentials(); + userCredentials.setId(new UserCredentialsId(userCredentialsUuid)); + userCredentials.setUserId(user.getId()); + userCredentials.setEnabled(false); + userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode()); + userCredentials.setActivateToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); + UserCredentialsUpdateMsg userCredentialsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials); + + return UplinkMsg.newBuilder() + .addUserUpdateMsg(userUpdateMsg) + .addUserCredentialsUpdateMsg(userCredentialsMsg) + .build(); + } + + private UplinkMsg constructUserCredentialsUplinkMsg(UserId userId, String password, boolean enabled, UUID userCredentialsUuid) { + UserCredentials userCredentials = new UserCredentials(); + userCredentials.setId(new UserCredentialsId(userCredentialsUuid)); + userCredentials.setUserId(userId); + userCredentials.setEnabled(enabled); + userCredentials.setPassword(password); + UserCredentialsUpdateMsg credsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials); + return UplinkMsg.newBuilder() + .addUserCredentialsUpdateMsg(credsMsg) + .build(); + } + + private void assertUserCredentialsFlags(User user, boolean enabled, boolean activated) { + JsonNode info = user.getAdditionalInfo(); + Assert.assertNotNull(info); + Assert.assertEquals(enabled, info.get("userCredentialsEnabled").asBoolean()); + Assert.assertEquals(activated, info.get("userActivated").asBoolean()); + } + + private UserUpdateMsg getLatestUserUpdateMsg() { + Optional opt = edgeImitator.findMessageByType(UserUpdateMsg.class); + Assert.assertTrue(opt.isPresent()); + return opt.get(); + } + } 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..1c8943ad7a 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 @@ -45,6 +45,8 @@ public interface UserService extends EntityDaoService { User saveUser(TenantId tenantId, User user); + User saveUser(TenantId tenantId, User user, boolean doValidate); + UserCredentials findUserCredentialsByUserId(TenantId tenantId, UserId userId); UserCredentials findUserCredentialsByActivateToken(TenantId tenantId, String activateToken); @@ -53,6 +55,8 @@ public interface UserService extends EntityDaoService { UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials); + UserCredentials saveUserCredentials(TenantId tenantId, UserCredentials userCredentials, boolean doValidate); + UserCredentials activateUserCredentials(TenantId tenantId, String activateToken, String password); UserCredentials requestPasswordReset(TenantId tenantId, String email); @@ -67,6 +71,9 @@ public interface UserService extends EntityDaoService { UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); + UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials, + UserCredentialsId oldUserCredentialsId, boolean doValidate); + void deleteUser(TenantId tenantId, User user); PageData findUsersByTenantId(TenantId tenantId, PageLink pageLink); diff --git a/common/edge-api/src/main/proto/edge.proto b/common/edge-api/src/main/proto/edge.proto index dbda462a99..7eddb326ef 100644 --- a/common/edge-api/src/main/proto/edge.proto +++ b/common/edge-api/src/main/proto/edge.proto @@ -441,6 +441,8 @@ message UplinkMsg { repeated RuleChainMetadataUpdateMsg ruleChainMetadataUpdateMsg = 24; repeated CalculatedFieldUpdateMsg calculatedFieldUpdateMsg = 25; repeated CalculatedFieldRequestMsg calculatedFieldRequestMsg = 26; + repeated UserUpdateMsg userUpdateMsg = 27; + repeated UserCredentialsUpdateMsg userCredentialsUpdateMsg = 28; } message UplinkResponseMsg { diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java index 777ce4dfc8..64d0fc21b8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java @@ -136,4 +136,5 @@ public class UserDataValidator extends DataValidator { } } } + } 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 5c94ba1891..8c229c815d 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 @@ -70,6 +70,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.UUID; import java.util.concurrent.TimeUnit; import static org.thingsboard.server.common.data.StringUtils.generateSafeToken; @@ -84,7 +85,7 @@ public class UserServiceImpl extends AbstractCachedEntityService tenantId); + if (doValidate) { + userCredentialsValidator.validate(userCredentials, data -> tenantId); + } UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); eventPublisher.publishEvent(ActionEntityEvent.builder() .tenantId(tenantId) @@ -304,19 +324,44 @@ public class UserServiceImpl extends AbstractCachedEntityService tenantId); - userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); - userCredentials.setId(null); - if (userCredentials.getPassword() != null) { - updatePasswordHistory(userCredentials); + return replaceUserCredentialsInternal(tenantId, userCredentials, userCredentials.getUuidId(), true); + } + + @Override + public UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials, + UserCredentialsId oldUserCredentialsId, boolean doValidate) { + return replaceUserCredentialsInternal(tenantId, userCredentials, oldUserCredentialsId.getId(), doValidate); + } + + private UserCredentials replaceUserCredentialsInternal(TenantId tenantId, UserCredentials userCredentials, + UUID oldCredentialsUuid, boolean doValidate) { + log.trace("[{}] Replacing user credentials for user [{}], old credentials ID [{}]", + tenantId, userCredentials.getUserId(), oldCredentialsUuid); + + if (doValidate) { + userCredentialsValidator.validate(userCredentials, data -> tenantId); + } + + try { + userCredentialsDao.removeById(tenantId, oldCredentialsUuid); + + if (userCredentials.getPassword() != null) { + updatePasswordHistory(userCredentials); + } + + UserCredentials savedCredentials = userCredentialsDao.save(tenantId, userCredentials); + + eventPublisher.publishEvent(ActionEntityEvent.builder() + .tenantId(tenantId) + .entityId(userCredentials.getUserId()) + .actionType(ActionType.CREDENTIALS_UPDATED) + .build()); + + return savedCredentials; + } catch (Exception e) { + log.error("[{}] Failed to replace user credentials for user [{}]", tenantId, userCredentials.getUserId(), e); + throw new RuntimeException("Failed to replace user credentials", e); } - UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); - eventPublisher.publishEvent(ActionEntityEvent.builder() - .tenantId(tenantId) - .entityId(userCredentials.getUserId()) - .actionType(ActionType.CREDENTIALS_UPDATED).build()); - return result; } @Override From c1b5febc754a4da4bfb14c496d7cb87ead67b709 Mon Sep 17 00:00:00 2001 From: Yevhenii Date: Wed, 8 Oct 2025 14:53:10 +0300 Subject: [PATCH 02/11] Refactor --- .../rpc/processor/user/BaseUserProcessor.java | 13 +- .../rpc/processor/user/UserEdgeProcessor.java | 2 +- .../thingsboard/server/edge/UserEdgeTest.java | 311 +++++++++--------- .../service/validator/UserDataValidator.java | 1 - .../server/dao/user/UserServiceImpl.java | 3 +- 5 files changed, 153 insertions(+), 177 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index 92d5664a4f..c836c14233 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -43,16 +43,12 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { try { User user = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); if (user == null) { - throw new RuntimeException("[{" + tenantId + "}] userUpdateMsg {" + userUpdateMsg + "} cannot be converted to user"); + throw new IllegalArgumentException(String.format("[%s] Failed to parse User from UserUpdateMsg: %s", tenantId, userUpdateMsg)); } User userById = edgeCtx.getUserService().findUserById(tenantId, userId); - if (userById == null) { - isCreated = true; - user.setId(null); - } else { - user.setId(userId); - } + isCreated = userById == null; + user.setId(isCreated ? null : userId); String userEmail = user.getEmail(); User existing = edgeCtx.getUserService().findUserByTenantIdAndEmail(tenantId, user.getEmail()); @@ -85,7 +81,7 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { protected void updateUserCredentials(TenantId tenantId, UserCredentialsUpdateMsg updateMsg) { UserCredentials userCredentialsFromUpdateMsg = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true); if (userCredentialsFromUpdateMsg == null) { - throw new RuntimeException(String.format("[%s] Failed to parse UserCredentials from updateMsg: %s", tenantId, updateMsg)); + throw new IllegalArgumentException(String.format("[%s] Failed to parse UserCredentials from updateMsg: %s", tenantId, updateMsg)); } User user = edgeCtx.getUserService().findUserById(tenantId, userCredentialsFromUpdateMsg.getUserId()); @@ -111,7 +107,6 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { updated.setPassword(userCredentialsFromUpdateMsg.getPassword()); updated.setResetToken(userCredentialsFromUpdateMsg.getResetToken()); - if (created) { edgeCtx.getUserService().saveUserCredentials(tenantId, updated, false); } else { diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index e0bd07c6c6..e7813caae9 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -96,7 +96,7 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso pushUserCreatedEventToRuleEngine(tenantId, edge, userId); } - Boolean userEmailUpdated = resultPair.getSecond(); + boolean userEmailUpdated = resultPair.getSecond(); if (userEmailUpdated) { saveEdgeEvent(tenantId, edge.getId(), EdgeEventType.USER, EdgeEventActionType.UPDATED, userId, null); diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 84442e2d0a..3e8abd645c 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -53,188 +53,75 @@ public class UserEdgeTest extends AbstractEdgeTest { private BCryptPasswordEncoder passwordEncoder; private static final String DEFAULT_FIRST_NAME = "Boris"; + private static final String DEFAULT_LAST_NAME = "Johnson"; private static final String UPDATED_LAST_NAME = "Borisov"; + private static final String DEFAULT_TENANT_ADMIN_EMAIL = "tenantAdmin@thingsboard.org"; + private static final String DEFAULT_CUSTOMER_USER_EMAIL = "customerUser@thingsboard.org"; @Test public void testCreateUpdateDeleteTenantUser() throws Exception { // create user - edgeImitator.expectMessageAmount(3); - User newTenantAdmin = buildUser(Authority.TENANT_ADMIN, null, "tenantAdmin@thingsboard.org", DEFAULT_FIRST_NAME, "Johnson"); - User savedTenantAdmin = createUser(newTenantAdmin, "tenant"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); - Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); - - UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); - User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); - Assert.assertNotNull(userMsg); - Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); + User newTenantAdmin = buildUser(Authority.TENANT_ADMIN, null); + User savedTenantAdmin = createAndVerifyUserOnEdge(newTenantAdmin); // update user - edgeImitator.expectMessageAmount(2); - savedTenantAdmin.setLastName(UPDATED_LAST_NAME); - savedTenantAdmin = doPost("/api/user", savedTenantAdmin, User.class); - Assert.assertTrue(edgeImitator.waitForMessages()); - - userUpdateMsg = getLatestUserUpdateMsg(); - User userFromMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); - Assert.assertNotNull(userFromMsg); - Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(UPDATED_LAST_NAME, userFromMsg.getLastName()); + updateAndVerifyUserLastName(savedTenantAdmin); // update user credentials login(savedTenantAdmin.getEmail(), "tenant"); - - edgeImitator.expectMessageAmount(1); - ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); - changePasswordRequest.setCurrentPassword("tenant"); - changePasswordRequest.setNewPassword("newTenant"); - doPost("/api/auth/changePassword", changePasswordRequest); - Assert.assertTrue(edgeImitator.waitForMessages()); - - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); - UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); - Assert.assertNotNull(userCredentialsMsg); - Assert.assertEquals(savedTenantAdmin.getId(), userCredentialsMsg.getUserId()); - Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsMsg.getPassword())); - + updateAndVerifyUserCredentials(savedTenantAdmin); loginTenantAdmin(); // delete user - edgeImitator.expectMessageAmount(1); - doDelete("/api/user/" + savedTenantAdmin.getUuidId()) - .andExpect(status().isOk()); - Assert.assertTrue(edgeImitator.waitForMessages()); - - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - userUpdateMsg = (UserUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); - Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); + deleteAndVerifyUser(savedTenantAdmin); } @Test public void testCreateUpdateDeleteCustomerUser() throws Exception { // create customer - Customer savedCustomer = createAndAssignCustomerToEdge("Edge Customer"); + Customer savedCustomer = createAndAssignCustomerToEdge(); // create user - edgeImitator.expectMessageAmount(3); - User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", "John", "Edwards"); - User savedCustomerUser = createUser(customerUser, "customer"); - Assert.assertTrue(edgeImitator.waitForMessages()); // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); - Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); - Optional userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); - Assert.assertTrue(userUpdateMsgOpt.isPresent()); - UserUpdateMsg userUpdateMsg = userUpdateMsgOpt.get(); - User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); - Assert.assertNotNull(userMsg); - Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomerUser.getId(), userMsg.getId()); - Assert.assertEquals(savedCustomerUser.getCustomerId(), userMsg.getCustomerId()); - Assert.assertEquals(savedCustomerUser.getAuthority(), userMsg.getAuthority()); - Assert.assertEquals(savedCustomerUser.getEmail(), userMsg.getEmail()); - Assert.assertEquals(savedCustomerUser.getFirstName(), userMsg.getFirstName()); - Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); + User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId()); + User savedCustomerUser = createAndVerifyUserOnEdge(customerUser); // update user - edgeImitator.expectMessageAmount(2); - savedCustomerUser.setLastName("Addams"); - savedCustomerUser = doPost("/api/user", savedCustomerUser, User.class); - Assert.assertTrue(edgeImitator.waitForMessages()); - userUpdateMsgOpt = edgeImitator.findMessageByType(UserUpdateMsg.class); - Assert.assertTrue(userUpdateMsgOpt.isPresent()); - userUpdateMsg = userUpdateMsgOpt.get(); - userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); - Assert.assertNotNull(userMsg); - Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomerUser.getLastName(), userMsg.getLastName()); + updateAndVerifyUserLastName(savedCustomerUser); // update user credentials login(savedCustomerUser.getEmail(), "customer"); - - edgeImitator.expectMessageAmount(1); - ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); - changePasswordRequest.setCurrentPassword("customer"); - changePasswordRequest.setNewPassword("newCustomer"); - doPost("/api/auth/changePassword", changePasswordRequest); - Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); - UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; - UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); - Assert.assertNotNull(userCredentialsMsg); - Assert.assertEquals(savedCustomerUser.getId(), userCredentialsMsg.getUserId()); - Assert.assertTrue(passwordEncoder.matches(changePasswordRequest.getNewPassword(), userCredentialsMsg.getPassword())); - + updateAndVerifyUserCredentials(savedCustomerUser); loginTenantAdmin(); // delete user - edgeImitator.expectMessageAmount(1); - doDelete("/api/user/" + savedCustomerUser.getUuidId()) - .andExpect(status().isOk()); - Assert.assertTrue(edgeImitator.waitForMessages()); - latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - userUpdateMsg = (UserUpdateMsg) latestMessage; - Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); - Assert.assertEquals(savedCustomerUser.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); - Assert.assertEquals(savedCustomerUser.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); - + deleteAndVerifyUser(savedCustomerUser); } @Test public void testSendUserToCloudFromEdge() throws Exception { // create customer - Customer savedCustomer = createAndAssignCustomerToEdge("Edge Customer"); - - // create user - User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", DEFAULT_FIRST_NAME, "Johnson"); - - UUID uuid = UUID.randomUUID(); - customerUser.setId(new UserId(uuid)); - UUID userCredentialsUuid = UUID.randomUUID(); - UplinkMsg uplinkMsg = constructUserUplinkMsg(customerUser, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userCredentialsUuid); + Customer savedCustomer = createAndAssignCustomerToEdge(); - edgeImitator.expectResponsesAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsg); - Assert.assertTrue(edgeImitator.waitForResponses()); - - User userFromCloud = doGet("/api/user/" + uuid, User.class); - Assert.assertNotNull(userFromCloud); - Assert.assertEquals(customerUser.getEmail(), userFromCloud.getEmail()); - //check user with existing email - User userWithExistingEmail = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId(), "customerUser@thingsboard.org", DEFAULT_FIRST_NAME, "Johnson"); - - UUID uuidForExistingEmail = UUID.randomUUID(); - userWithExistingEmail.setId(new UserId(uuidForExistingEmail)); - UUID userCredentialsUuidForExistingEmail = UUID.randomUUID(); - UplinkMsg uplinkMsgForExistingEmail = constructUserUplinkMsg(userWithExistingEmail, UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userCredentialsUuidForExistingEmail); - - edgeImitator.expectResponsesAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsgForExistingEmail); - Assert.assertTrue(edgeImitator.waitForResponses()); - - User userFromCloudWithExistingEmail = doGet("/api/user/" + uuidForExistingEmail, User.class); - Assert.assertNotNull(userFromCloudWithExistingEmail); - Assert.assertNotEquals(userWithExistingEmail.getEmail(), userFromCloudWithExistingEmail.getEmail()); + // create uplinkMsg with user and userCredentials + UserId userId = new UserId(UUID.randomUUID()); + UserCredentialsId userCredentialsId = new UserCredentialsId(UUID.randomUUID()); + UplinkMsg uplinkMsg = buildUserUplinkMsg(userId, savedCustomer.getId(), userCredentialsId); + User userFromCloud = verifyMsgOnCloud(uplinkMsg, userId, false); assertUserCredentialsFlags(userFromCloud, false, false); - UplinkMsg enabledCredentialsUplinkMsg = constructUserCredentialsUplinkMsg(customerUser.getId(), "password", true, userCredentialsUuid); + // create uplinkMsg with enabled userCredentials + UplinkMsg uplinkMsgWithEnabledCredentials = constructUserCredentialsUplinkMsg(userCredentialsId, userId); - edgeImitator.expectResponsesAmount(1); - edgeImitator.sendUplinkMsg(enabledCredentialsUplinkMsg); - Assert.assertTrue(edgeImitator.waitForResponses()); + User cloudUserWithCredentials = verifyMsgOnCloud(uplinkMsgWithEnabledCredentials, userId, false); + assertUserCredentialsFlags(cloudUserWithCredentials, true, true); - User cloudUserWithCredentials = doGet("/api/user/" + uuid, User.class); - Assert.assertNotNull(cloudUserWithCredentials); + // create uplinkMsg with user the same email + UserId secondUserId = new UserId(UUID.randomUUID()); + UserCredentialsId secondCredentialsId = new UserCredentialsId(UUID.randomUUID()); + UplinkMsg uplinkMsgForUserExistingEmail = buildUserUplinkMsg(secondUserId, savedCustomer.getId(), secondCredentialsId); - assertUserCredentialsFlags(cloudUserWithCredentials, true, true); + verifyMsgOnCloud(uplinkMsgForUserExistingEmail, secondUserId, true); } @Test @@ -264,40 +151,111 @@ public class UserEdgeTest extends AbstractEdgeTest { testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); } - private User buildUser(Authority authority, CustomerId customerId, String email, String firstName, String lastName) { + private Customer createAndAssignCustomerToEdge() throws Exception { + edgeImitator.expectMessageAmount(1); + Customer customer = new Customer(); + customer.setTitle("Edge Customer"); + Customer savedCustomer = doPost("/api/customer", customer, Customer.class); + Assert.assertFalse(edgeImitator.waitForMessages(5)); + + edgeImitator.expectMessageAmount(2); + doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class); + Assert.assertTrue(edgeImitator.waitForMessages()); + + return savedCustomer; + } + + private User buildUser(Authority authority, CustomerId customerId) { User customerUser = new User(); + customerUser.setAuthority(authority); customerUser.setTenantId(tenantId); customerUser.setCustomerId(customerId); - customerUser.setEmail(email); - customerUser.setFirstName(firstName); - customerUser.setLastName(lastName); + customerUser.setEmail(authority == Authority.TENANT_ADMIN ? DEFAULT_TENANT_ADMIN_EMAIL : DEFAULT_CUSTOMER_USER_EMAIL); + customerUser.setFirstName(DEFAULT_FIRST_NAME); + customerUser.setLastName(DEFAULT_LAST_NAME); + return customerUser; } - private Customer createAndAssignCustomerToEdge(String title) throws Exception { - edgeImitator.expectMessageAmount(1); - Customer customer = new Customer(); - customer.setTitle(title); - Customer savedCustomer = doPost("/api/customer", customer, Customer.class); - Assert.assertFalse(edgeImitator.waitForMessages(5)); + private User createAndVerifyUserOnEdge(User user) throws Exception { + // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) + edgeImitator.expectMessageAmount(3); + User savedUser = createUser(user, user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer"); + Assert.assertTrue(edgeImitator.waitForMessages()); + + Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); + Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); + + UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); + User userMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); + Assert.assertNotNull(userMsg); + Assert.assertEquals(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); + Assert.assertEquals(savedUser.getId(), userMsg.getId()); + Assert.assertEquals(savedUser.getCustomerId(), userMsg.getCustomerId()); + Assert.assertEquals(savedUser.getAuthority(), userMsg.getAuthority()); + Assert.assertEquals(savedUser.getEmail(), userMsg.getEmail()); + Assert.assertEquals(savedUser.getFirstName(), userMsg.getFirstName()); + Assert.assertEquals(savedUser.getLastName(), userMsg.getLastName()); + + return savedUser; + } + + private void updateAndVerifyUserLastName(User user) throws Exception { + user.setLastName(UPDATED_LAST_NAME); edgeImitator.expectMessageAmount(2); - doPost("/api/customer/" + savedCustomer.getUuidId() + "/edge/" + edge.getUuidId(), Edge.class); + doPost("/api/user", user, User.class); Assert.assertTrue(edgeImitator.waitForMessages()); - return savedCustomer; + UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); + User userFromMsg = JacksonUtil.fromString(userUpdateMsg.getEntity(), User.class, true); + Assert.assertNotNull(userFromMsg); + Assert.assertEquals(UpdateMsgType.ENTITY_UPDATED_RPC_MESSAGE, userUpdateMsg.getMsgType()); + Assert.assertEquals(UPDATED_LAST_NAME, userFromMsg.getLastName()); } - private UplinkMsg constructUserUplinkMsg(User user, UpdateMsgType msgType, UUID userCredentialsUuid) { - UserUpdateMsg userUpdateMsg = EdgeMsgConstructorUtils.constructUserUpdatedMsg(msgType, user); + private void updateAndVerifyUserCredentials(User user) throws Exception { + String password = user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer"; + String newPassword = "new" + password; - UserCredentials userCredentials = new UserCredentials(); - userCredentials.setId(new UserCredentialsId(userCredentialsUuid)); - userCredentials.setUserId(user.getId()); - userCredentials.setEnabled(false); - userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode()); + edgeImitator.expectMessageAmount(1); + ChangePasswordRequest changePasswordRequest = new ChangePasswordRequest(); + changePasswordRequest.setCurrentPassword(password); + changePasswordRequest.setNewPassword(newPassword); + doPost("/api/auth/changePassword", changePasswordRequest); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); + UserCredentialsUpdateMsg msg = (UserCredentialsUpdateMsg) latestMessage; + UserCredentials creds = JacksonUtil.fromString(msg.getEntity(), UserCredentials.class, true); + Assert.assertNotNull(creds); + Assert.assertEquals(user.getId(), creds.getUserId()); + Assert.assertTrue(passwordEncoder.matches(newPassword, creds.getPassword())); + } + + private void deleteAndVerifyUser(User savedTenantAdmin) throws Exception { + edgeImitator.expectMessageAmount(1); + doDelete("/api/user/" + savedTenantAdmin.getUuidId()) + .andExpect(status().isOk()); + Assert.assertTrue(edgeImitator.waitForMessages()); + + AbstractMessage latestMessage = edgeImitator.getLatestMessage(); + Assert.assertTrue(latestMessage instanceof UserUpdateMsg); + UserUpdateMsg userUpdateMsg = (UserUpdateMsg) latestMessage; + Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); + Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); + Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); + } + + private UplinkMsg buildUserUplinkMsg(UserId userId, CustomerId customerId, UserCredentialsId userCredentialsUuid) { + User customerUser = buildUser(Authority.CUSTOMER_USER, customerId); + customerUser.setId(userId); + UserCredentials userCredentials = buildCredentials(userCredentialsUuid, userId, false); userCredentials.setActivateToken(StringUtils.randomAlphanumeric(DEFAULT_TOKEN_LENGTH)); + + UserUpdateMsg userUpdateMsg = EdgeMsgConstructorUtils.constructUserUpdatedMsg(UpdateMsgType.ENTITY_CREATED_RPC_MESSAGE, customerUser); UserCredentialsUpdateMsg userCredentialsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials); return UplinkMsg.newBuilder() @@ -306,13 +264,38 @@ public class UserEdgeTest extends AbstractEdgeTest { .build(); } - private UplinkMsg constructUserCredentialsUplinkMsg(UserId userId, String password, boolean enabled, UUID userCredentialsUuid) { + private UserCredentials buildCredentials(UserCredentialsId userCredentialsUuid, UserId userId, boolean enabled) { UserCredentials userCredentials = new UserCredentials(); - userCredentials.setId(new UserCredentialsId(userCredentialsUuid)); + + userCredentials.setId(userCredentialsUuid); userCredentials.setUserId(userId); userCredentials.setEnabled(enabled); - userCredentials.setPassword(password); + userCredentials.setAdditionalInfo(JacksonUtil.newObjectNode()); + + return userCredentials; + } + + private User verifyMsgOnCloud(UplinkMsg uplinkMsg, UserId userId, boolean emailExist) throws Exception { + edgeImitator.expectResponsesAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsg); + Assert.assertTrue(edgeImitator.waitForResponses()); + + User userFromCloud = doGet("/api/user/" + userId, User.class); + Assert.assertNotNull(userFromCloud); + + if (emailExist) { + Assert.assertNotEquals(DEFAULT_CUSTOMER_USER_EMAIL, userFromCloud.getEmail()); + } else { + Assert.assertEquals(DEFAULT_CUSTOMER_USER_EMAIL, userFromCloud.getEmail()); + } + return userFromCloud; + } + + private UplinkMsg constructUserCredentialsUplinkMsg(UserCredentialsId userCredentialsUuid, UserId userId) { + UserCredentials userCredentials = buildCredentials(userCredentialsUuid, userId, true); + userCredentials.setPassword("password"); UserCredentialsUpdateMsg credsMsg = EdgeMsgConstructorUtils.constructUserCredentialsUpdatedMsg(userCredentials); + return UplinkMsg.newBuilder() .addUserCredentialsUpdateMsg(credsMsg) .build(); diff --git a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java index 64d0fc21b8..777ce4dfc8 100644 --- a/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java +++ b/dao/src/main/java/org/thingsboard/server/dao/service/validator/UserDataValidator.java @@ -136,5 +136,4 @@ public class UserDataValidator extends DataValidator { } } } - } 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 8c229c815d..7cbf1c80bd 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 @@ -166,7 +166,6 @@ public class UserServiceImpl extends AbstractCachedEntityService Date: Thu, 9 Oct 2025 13:10:58 +0300 Subject: [PATCH 03/11] Refactor Test --- .../thingsboard/server/edge/UserEdgeTest.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 3e8abd645c..8b5261150b 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -16,7 +16,6 @@ package org.thingsboard.server.edge; import com.fasterxml.jackson.databind.JsonNode; -import com.google.protobuf.AbstractMessage; import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -125,7 +124,7 @@ public class UserEdgeTest extends AbstractEdgeTest { } @Test - public void testSendUserCredentialsRequest() throws Exception { + public void testSendUserCredentialsRequestToCloud() throws Exception { UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); UserCredentialsRequestMsg.Builder userCredentialsRequestMsgBuilder = UserCredentialsRequestMsg.newBuilder(); userCredentialsRequestMsgBuilder.setUserIdMSB(tenantAdminUserId.getId().getMostSignificantBits()); @@ -141,9 +140,7 @@ public class UserEdgeTest extends AbstractEdgeTest { Assert.assertTrue(edgeImitator.waitForResponses()); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); - UserCredentialsUpdateMsg userCredentialsUpdateMsg = (UserCredentialsUpdateMsg) latestMessage; + UserCredentialsUpdateMsg userCredentialsUpdateMsg = getLatestUserCredentialsUpdateMsg(); UserCredentials userCredentialsMsg = JacksonUtil.fromString(userCredentialsUpdateMsg.getEntity(), UserCredentials.class, true); Assert.assertNotNull(userCredentialsMsg); Assert.assertEquals(tenantAdminUserId, userCredentialsMsg.getUserId()); @@ -166,24 +163,25 @@ public class UserEdgeTest extends AbstractEdgeTest { } private User buildUser(Authority authority, CustomerId customerId) { - User customerUser = new User(); + User user = new User(); - customerUser.setAuthority(authority); - customerUser.setTenantId(tenantId); - customerUser.setCustomerId(customerId); - customerUser.setEmail(authority == Authority.TENANT_ADMIN ? DEFAULT_TENANT_ADMIN_EMAIL : DEFAULT_CUSTOMER_USER_EMAIL); - customerUser.setFirstName(DEFAULT_FIRST_NAME); - customerUser.setLastName(DEFAULT_LAST_NAME); + user.setAuthority(authority); + user.setTenantId(tenantId); + user.setCustomerId(customerId); + user.setEmail(authority == Authority.TENANT_ADMIN ? DEFAULT_TENANT_ADMIN_EMAIL : DEFAULT_CUSTOMER_USER_EMAIL); + user.setFirstName(DEFAULT_FIRST_NAME); + user.setLastName(DEFAULT_LAST_NAME); - return customerUser; + return user; } private User createAndVerifyUserOnEdge(User user) throws Exception { + String password = user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer"; + // wait 3 messages - x1 user update msg and x2 user credentials update msgs (create + authenticate user) edgeImitator.expectMessageAmount(3); - User savedUser = createUser(user, user.getAuthority() == Authority.TENANT_ADMIN ? "tenant" : "customer"); + User savedUser = createUser(user, password); Assert.assertTrue(edgeImitator.waitForMessages()); - Assert.assertEquals(1, edgeImitator.findAllMessagesByType(UserUpdateMsg.class).size()); Assert.assertEquals(2, edgeImitator.findAllMessagesByType(UserCredentialsUpdateMsg.class).size()); @@ -226,9 +224,7 @@ public class UserEdgeTest extends AbstractEdgeTest { doPost("/api/auth/changePassword", changePasswordRequest); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserCredentialsUpdateMsg); - UserCredentialsUpdateMsg msg = (UserCredentialsUpdateMsg) latestMessage; + UserCredentialsUpdateMsg msg = getLatestUserCredentialsUpdateMsg(); UserCredentials creds = JacksonUtil.fromString(msg.getEntity(), UserCredentials.class, true); Assert.assertNotNull(creds); Assert.assertEquals(user.getId(), creds.getUserId()); @@ -241,9 +237,7 @@ public class UserEdgeTest extends AbstractEdgeTest { .andExpect(status().isOk()); Assert.assertTrue(edgeImitator.waitForMessages()); - AbstractMessage latestMessage = edgeImitator.getLatestMessage(); - Assert.assertTrue(latestMessage instanceof UserUpdateMsg); - UserUpdateMsg userUpdateMsg = (UserUpdateMsg) latestMessage; + UserUpdateMsg userUpdateMsg = getLatestUserUpdateMsg(); Assert.assertEquals(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE, userUpdateMsg.getMsgType()); Assert.assertEquals(savedTenantAdmin.getUuidId().getMostSignificantBits(), userUpdateMsg.getIdMSB()); Assert.assertEquals(savedTenantAdmin.getUuidId().getLeastSignificantBits(), userUpdateMsg.getIdLSB()); @@ -314,4 +308,10 @@ public class UserEdgeTest extends AbstractEdgeTest { return opt.get(); } + private UserCredentialsUpdateMsg getLatestUserCredentialsUpdateMsg() { + Optional opt = edgeImitator.findMessageByType(UserCredentialsUpdateMsg.class); + Assert.assertTrue(opt.isPresent()); + return opt.get(); + } + } From 9085b42d4304c000f5a66cfab275899458300ce1 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Fri, 21 Nov 2025 14:26:47 +0200 Subject: [PATCH 04/11] Add handling of ENTITY_DELETED_RPC_MESSAGE during edge user message processing --- .../rpc/processor/user/BaseUserProcessor.java | 10 +++++++- .../rpc/processor/user/UserEdgeProcessor.java | 14 ++++++++++- .../thingsboard/server/edge/UserEdgeTest.java | 25 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index c836c14233..14e4da53c0 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -78,6 +78,15 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, userEmailUpdated); } + protected User deleteUser(TenantId tenantId, UserId userId) { + User userById = edgeCtx.getUserService().findUserById(tenantId, userId); + if (userById == null) { + throw new IllegalArgumentException(String.format("[%s] Failed to find User with id [%s]", tenantId, userId)); + } + edgeCtx.getUserService().deleteUser(tenantId, userById); + return userById; + } + protected void updateUserCredentials(TenantId tenantId, UserCredentialsUpdateMsg updateMsg) { UserCredentials userCredentialsFromUpdateMsg = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true); if (userCredentialsFromUpdateMsg == null) { @@ -117,7 +126,6 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { tenantId, user.getName(), updateMsg, e); throw new RuntimeException(e); } - } protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg); diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index e7813caae9..cfe888b1a5 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -61,6 +61,10 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso saveOrUpdateUser(tenantId, userId, userUpdateMsg, edge); yield Futures.immediateFuture(null); } + case ENTITY_DELETED_RPC_MESSAGE -> { + deleteUserAndPushEntityDeletedEventToRuleEngine(tenantId, userId, edge); + yield Futures.immediateFuture(null); + } default -> handleUnsupportedMsgType(userUpdateMsg.getMsgType()); }; } catch (DataValidationException e) { @@ -116,7 +120,15 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso } } - @Override + private void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId, Edge edge) { + User removedUser = deleteUser(tenantId, userId); + CustomerId userCustomerId = removedUser.getCustomerId(); + String userAsString = JacksonUtil.toString(removedUser); + TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, userCustomerId); + pushEntityEventToRuleEngine(tenantId, userId, userCustomerId, TbMsgType.ENTITY_DELETED, userAsString, msgMetaData); + } + + @Override public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UserId userId = new UserId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index 8b5261150b..f590b21b84 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -148,6 +148,31 @@ public class UserEdgeTest extends AbstractEdgeTest { testAutoGeneratedCodeByProtobuf(userCredentialsUpdateMsg); } + @Test + public void testSendUserDeleteFromEdgeToCloud() throws Exception { + // create customer + Customer savedCustomer = createAndAssignCustomerToEdge(); + + // create user + User customerUser = buildUser(Authority.CUSTOMER_USER, savedCustomer.getId()); + User savedCustomerUser = createAndVerifyUserOnEdge(customerUser); + + // simulate user removal event from edge to cloud + UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); + UserUpdateMsg.newBuilder().setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + .setIdMSB(savedCustomerUser.getUuidId().getMostSignificantBits()) + .setIdLSB(savedCustomerUser.getUuidId().getLeastSignificantBits()); + + testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + + // expect edge message sent & cloud message response + edgeImitator.expectResponsesAmount(1); + edgeImitator.expectMessageAmount(1); + edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + Assert.assertTrue(edgeImitator.waitForResponses()); + Assert.assertTrue(edgeImitator.waitForMessages()); + } + private Customer createAndAssignCustomerToEdge() throws Exception { edgeImitator.expectMessageAmount(1); Customer customer = new Customer(); From 12ed2a56be5e728a6e8e3f127db40c534fa595c5 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Fri, 21 Nov 2025 15:40:37 +0200 Subject: [PATCH 05/11] Remove exception when user doesn't exist during edge user delete event processing --- .../service/edge/rpc/processor/user/BaseUserProcessor.java | 3 ++- .../service/edge/rpc/processor/user/UserEdgeProcessor.java | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index 14e4da53c0..897e49a7cd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -81,7 +81,8 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { protected User deleteUser(TenantId tenantId, UserId userId) { User userById = edgeCtx.getUserService().findUserById(tenantId, userId); if (userById == null) { - throw new IllegalArgumentException(String.format("[%s] Failed to find User with id [%s]", tenantId, userId)); + log.trace("[{}] User with id {} does not exist", tenantId, userId); + return null; } edgeCtx.getUserService().deleteUser(tenantId, userById); return userById; diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index cfe888b1a5..7905e802bd 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -122,6 +122,9 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso private void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId, Edge edge) { User removedUser = deleteUser(tenantId, userId); + if (removedUser == null) { + return; + } CustomerId userCustomerId = removedUser.getCustomerId(); String userAsString = JacksonUtil.toString(removedUser); TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, userCustomerId); From bc1af0ba3bc290ecd7bf5f95a01782a977be5171 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Fri, 21 Nov 2025 17:04:42 +0200 Subject: [PATCH 06/11] Fix testSendUserDeleteFromEdgeToCloud --- .../rpc/processor/user/UserEdgeProcessor.java | 2 +- .../thingsboard/server/edge/UserEdgeTest.java | 28 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index 7905e802bd..968d573618 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -131,7 +131,7 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso pushEntityEventToRuleEngine(tenantId, userId, userCustomerId, TbMsgType.ENTITY_DELETED, userAsString, msgMetaData); } - @Override + @Override public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UserId userId = new UserId(edgeEvent.getEntityId()); switch (edgeEvent.getAction()) { diff --git a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java index f590b21b84..6bf51eb159 100644 --- a/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java +++ b/application/src/test/java/org/thingsboard/server/edge/UserEdgeTest.java @@ -20,8 +20,11 @@ import org.junit.Assert; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.test.web.servlet.ResultMatcher; +import org.testcontainers.shaded.org.awaitility.Awaitility; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.Customer; +import org.thingsboard.server.common.data.EdgeUtils; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.edge.Edge; @@ -41,6 +44,7 @@ import org.thingsboard.server.service.security.model.ChangePasswordRequest; import java.util.Optional; import java.util.UUID; +import java.util.concurrent.TimeUnit; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import static org.thingsboard.server.dao.user.UserServiceImpl.DEFAULT_TOKEN_LENGTH; @@ -158,19 +162,31 @@ public class UserEdgeTest extends AbstractEdgeTest { User savedCustomerUser = createAndVerifyUserOnEdge(customerUser); // simulate user removal event from edge to cloud - UplinkMsg.Builder uplinkMsgBuilder = UplinkMsg.newBuilder(); - UserUpdateMsg.newBuilder().setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) + UserUpdateMsg.Builder userUpdateMsg = UserUpdateMsg.newBuilder().setMsgType(UpdateMsgType.ENTITY_DELETED_RPC_MESSAGE) .setIdMSB(savedCustomerUser.getUuidId().getMostSignificantBits()) .setIdLSB(savedCustomerUser.getUuidId().getLeastSignificantBits()); - testAutoGeneratedCodeByProtobuf(uplinkMsgBuilder); + UplinkMsg uplink = UplinkMsg.newBuilder() + .setUplinkMsgId(EdgeUtils.nextPositiveInt()) + .addUserUpdateMsg(userUpdateMsg).build(); + + testAutoGeneratedCodeByProtobuf(userUpdateMsg); // expect edge message sent & cloud message response edgeImitator.expectResponsesAmount(1); - edgeImitator.expectMessageAmount(1); - edgeImitator.sendUplinkMsg(uplinkMsgBuilder.build()); + edgeImitator.sendUplinkMsg(uplink); Assert.assertTrue(edgeImitator.waitForResponses()); - Assert.assertTrue(edgeImitator.waitForMessages()); + + loginTenantAdmin(); + Awaitility.await().atMost(10, TimeUnit.SECONDS) + .until(() -> { + try { + doGet("/api/user/" + savedCustomerUser.getId(), User.class, status().isNotFound()); + return true; + } catch (Throwable ex) { + return false; + } + }); } private Customer createAndAssignCustomerToEdge() throws Exception { From 1315733d1e2db20c1f904dbd5db1a120eddefd45 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Mon, 24 Nov 2025 12:11:41 +0200 Subject: [PATCH 07/11] Revert userCredentials.setId(null); in UserServiceImpl#replaceUserCredentialsInternal --- .../java/org/thingsboard/server/dao/user/UserServiceImpl.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f2b9e476b5..bdcac80d4a 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 @@ -361,7 +361,7 @@ public class UserServiceImpl extends AbstractCachedEntityService Date: Mon, 24 Nov 2025 14:23:26 +0200 Subject: [PATCH 08/11] Fix overriding exiting user credential ID before calling replaceUserCredentials on BaseUserProcessor --- .../service/edge/rpc/processor/user/BaseUserProcessor.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index 897e49a7cd..0da0e4aade 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -23,6 +23,7 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; +import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.service.DataValidator; @@ -107,6 +108,7 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { try { UserCredentials existing = edgeCtx.getUserService().findUserCredentialsByUserId(tenantId, user.getId()); boolean created = existing == null; + UserCredentialsId oldCredentialsId = created ? null : existing.getId(); UserCredentials updated = created ? new UserCredentials() : existing; updated.setId(userCredentialsFromUpdateMsg.getId()); @@ -120,7 +122,7 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { if (created) { edgeCtx.getUserService().saveUserCredentials(tenantId, updated, false); } else { - edgeCtx.getUserService().replaceUserCredentials(tenantId, updated, existing.getId(), false); + edgeCtx.getUserService().replaceUserCredentials(tenantId, updated, oldCredentialsId, false); } } catch (Exception e) { log.error("[{}] Can't update user credentials for user [{}], userCredentialsUpdateMsg [{}]", From 7fb7e3f24c1a08c750ce7c4c82932286d0d4feb6 Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 25 Nov 2025 16:53:39 +0200 Subject: [PATCH 09/11] Refactoring - revert replaceUserCredentials. Added removeUserCredentials and use it instead --- .../rpc/processor/user/BaseUserProcessor.java | 35 +++--------- .../server/dao/user/UserService.java | 3 +- .../server/dao/user/UserServiceImpl.java | 56 +++++++------------ 3 files changed, 30 insertions(+), 64 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index 0da0e4aade..fe640505b1 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -23,7 +23,6 @@ import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; -import org.thingsboard.server.common.data.id.UserCredentialsId; import org.thingsboard.server.common.data.id.UserId; import org.thingsboard.server.common.data.security.UserCredentials; import org.thingsboard.server.dao.service.DataValidator; @@ -90,40 +89,24 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { } protected void updateUserCredentials(TenantId tenantId, UserCredentialsUpdateMsg updateMsg) { - UserCredentials userCredentialsFromUpdateMsg = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true); - if (userCredentialsFromUpdateMsg == null) { + UserCredentials userCredentials = JacksonUtil.fromString(updateMsg.getEntity(), UserCredentials.class, true); + if (userCredentials == null) { throw new IllegalArgumentException(String.format("[%s] Failed to parse UserCredentials from updateMsg: %s", tenantId, updateMsg)); } - - User user = edgeCtx.getUserService().findUserById(tenantId, userCredentialsFromUpdateMsg.getUserId()); + User user = edgeCtx.getUserService().findUserById(tenantId, userCredentials.getUserId()); if (user == null) { log.warn("[{}] Can't find user by id [{}] skipping credentials update. UserCredentialsUpdateMsg [{}]", - tenantId, userCredentialsFromUpdateMsg.getUserId(), updateMsg); + tenantId, userCredentials.getUserId(), updateMsg); return; } - log.debug("[{}] Updating user credentials for user [{}]. New credentials Id [{}], enabled [{}]", - tenantId, user.getName(), userCredentialsFromUpdateMsg.getId(), userCredentialsFromUpdateMsg.isEnabled()); - + tenantId, user.getName(), userCredentials.getId(), userCredentials.isEnabled()); try { - UserCredentials existing = edgeCtx.getUserService().findUserCredentialsByUserId(tenantId, user.getId()); - boolean created = existing == null; - UserCredentialsId oldCredentialsId = created ? null : existing.getId(); - - UserCredentials updated = created ? new UserCredentials() : existing; - updated.setId(userCredentialsFromUpdateMsg.getId()); - updated.setUserId(user.getId()); - updated.setEnabled(userCredentialsFromUpdateMsg.isEnabled()); - updated.setActivateToken(userCredentialsFromUpdateMsg.getActivateToken()); - updated.setAdditionalInfo(userCredentialsFromUpdateMsg.getAdditionalInfo()); - updated.setPassword(userCredentialsFromUpdateMsg.getPassword()); - updated.setResetToken(userCredentialsFromUpdateMsg.getResetToken()); - - if (created) { - edgeCtx.getUserService().saveUserCredentials(tenantId, updated, false); - } else { - edgeCtx.getUserService().replaceUserCredentials(tenantId, updated, oldCredentialsId, false); + UserCredentials userCredentialsByUserId = edgeCtx.getUserService().findUserCredentialsByUserId(tenantId, user.getId()); + if (userCredentialsByUserId != null && !userCredentialsByUserId.getId().equals(userCredentials.getId())) { + edgeCtx.getUserService().deleteUserCredentials(tenantId, userCredentialsByUserId); } + edgeCtx.getUserService().saveUserCredentials(tenantId, userCredentials, false); } catch (Exception e) { log.error("[{}] Can't update user credentials for user [{}], userCredentialsUpdateMsg [{}]", tenantId, user.getName(), updateMsg, e); 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 c3f7b72ed3..1062982885 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 @@ -74,8 +74,7 @@ public interface UserService extends EntityDaoService { UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials); - UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials, - UserCredentialsId oldUserCredentialsId, boolean doValidate); + void deleteUserCredentials(TenantId tenantId, UserCredentials userCredentials); void deleteUser(TenantId tenantId, User user); 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 bdcac80d4a..e10d87e760 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 @@ -79,7 +79,6 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -99,6 +98,7 @@ public class UserServiceImpl extends AbstractCachedEntityService tenantId); + userCredentialsDao.removeById(tenantId, userCredentials.getUuidId()); + userCredentials.setId(null); + if (userCredentials.getPassword() != null) { + updatePasswordHistory(userCredentials); + } + UserCredentials result = userCredentialsDao.save(tenantId, userCredentials); + eventPublisher.publishEvent(ActionEntityEvent.builder() + .tenantId(tenantId) + .entityId(userCredentials.getUserId()) + .actionType(ActionType.CREDENTIALS_UPDATED).build()); + return result; } @Override - public UserCredentials replaceUserCredentials(TenantId tenantId, UserCredentials userCredentials, - UserCredentialsId oldUserCredentialsId, boolean doValidate) { - return replaceUserCredentialsInternal(tenantId, userCredentials, oldUserCredentialsId.getId(), doValidate); - } - - private UserCredentials replaceUserCredentialsInternal(TenantId tenantId, UserCredentials userCredentials, - UUID oldCredentialsUuid, boolean doValidate) { - log.trace("[{}] Replacing user credentials for user [{}], old credentials ID [{}]", - tenantId, userCredentials.getUserId(), oldCredentialsUuid); - - if (doValidate) { - userCredentialsValidator.validate(userCredentials, data -> tenantId); - } - - try { - userCredentialsDao.removeById(tenantId, oldCredentialsUuid); - userCredentials.setId(null); - if (userCredentials.getPassword() != null) { - updatePasswordHistory(userCredentials); - } - - UserCredentials savedCredentials = userCredentialsDao.save(tenantId, userCredentials); - - eventPublisher.publishEvent(ActionEntityEvent.builder() - .tenantId(tenantId) - .entityId(userCredentials.getUserId()) - .actionType(ActionType.CREDENTIALS_UPDATED) - .build()); - - return savedCredentials; - } catch (Exception e) { - log.error("[{}] Failed to replace user credentials for user [{}]", tenantId, userCredentials.getUserId(), e); - throw new RuntimeException("Failed to replace user credentials", e); - } + public void deleteUserCredentials(TenantId tenantId, UserCredentials userCredentials) { + Objects.requireNonNull(userCredentials, "UserCredentials is null"); + UserCredentialsId userCredentialsId = userCredentials.getId(); + log.trace("[{}] Executing deleteUserCredentials [{}]", tenantId, userCredentialsId); + validateId(userCredentialsId, id -> INCORRECT_USER_CREDENTIALS_ID + id); + userCredentialsDao.removeById(tenantId, userCredentialsId.getId()); } @Override From c5a8a874a97e20c67db49ea12b948cfb4d8c508e Mon Sep 17 00:00:00 2001 From: Volodymyr Babak Date: Tue, 25 Nov 2025 17:00:29 +0200 Subject: [PATCH 10/11] refacotring to be in sync with PE --- .../edge/rpc/processor/user/BaseUserProcessor.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index fe640505b1..a742d83ada 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -47,8 +47,12 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { } User userById = edgeCtx.getUserService().findUserById(tenantId, userId); - isCreated = userById == null; - user.setId(isCreated ? null : userId); + if (userById == null) { + isCreated = true; + user.setId(null); + } else { + user.setId(userId); + } String userEmail = user.getEmail(); User existing = edgeCtx.getUserService().findUserByTenantIdAndEmail(tenantId, user.getEmail()); From c35288af5fadbbbb779dc1b0053db56d22f86d45 Mon Sep 17 00:00:00 2001 From: Nikita Mazurenko Date: Wed, 26 Nov 2025 19:27:09 +0200 Subject: [PATCH 11/11] Refactor deleteUserAndPushEntityDeletedEventToRuleEngine to add additional metadata --- .../rpc/processor/user/BaseUserProcessor.java | 34 ++++++++++++++++++- .../rpc/processor/user/UserEdgeProcessor.java | 11 ------ 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java index a742d83ada..fc24588f5b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/BaseUserProcessor.java @@ -21,10 +21,14 @@ import org.springframework.data.util.Pair; import org.thingsboard.common.util.JacksonUtil; import org.thingsboard.server.common.data.StringUtils; import org.thingsboard.server.common.data.User; +import org.thingsboard.server.common.data.edge.Edge; +import org.thingsboard.server.common.data.exception.ThingsboardException; import org.thingsboard.server.common.data.id.CustomerId; import org.thingsboard.server.common.data.id.TenantId; import org.thingsboard.server.common.data.id.UserId; +import org.thingsboard.server.common.data.msg.TbMsgType; import org.thingsboard.server.common.data.security.UserCredentials; +import org.thingsboard.server.common.msg.TbMsgMetaData; import org.thingsboard.server.dao.service.DataValidator; import org.thingsboard.server.gen.edge.v1.UserCredentialsUpdateMsg; import org.thingsboard.server.gen.edge.v1.UserUpdateMsg; @@ -82,7 +86,23 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { return Pair.of(isCreated, userEmailUpdated); } - protected User deleteUser(TenantId tenantId, UserId userId) { + protected void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId) { + deleteUserAndPushEntityDeletedEventToRuleEngine(tenantId, userId, null); + } + + protected void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId, Edge edge) { + User removedUser = deleteUser(tenantId, userId); + if (removedUser == null) { + return; + } + CustomerId userCustomerId = removedUser.getCustomerId(); + String userAsString = JacksonUtil.toString(removedUser); + TbMsgMetaData msgMetaData = edge == null ? new TbMsgMetaData() : getEdgeActionTbMsgMetaData(edge, userCustomerId); + addRemovedUserMetadata(msgMetaData, removedUser); + pushEntityEventToRuleEngine(tenantId, userId, userCustomerId, TbMsgType.ENTITY_DELETED, userAsString, msgMetaData); + } + + private User deleteUser(TenantId tenantId, UserId userId) { User userById = edgeCtx.getUserService().findUserById(tenantId, userId); if (userById == null) { log.trace("[{}] User with id {} does not exist", tenantId, userId); @@ -118,6 +138,18 @@ public abstract class BaseUserProcessor extends BaseEdgeProcessor { } } + private void addRemovedUserMetadata(TbMsgMetaData metaData, User removedUser) { + metaData.putValue("userId", removedUser.getId().toString()); + metaData.putValue("userName", removedUser.getName()); + metaData.putValue("userEmail", removedUser.getEmail()); + if (removedUser.getFirstName() != null) { + metaData.putValue("userFirstName", removedUser.getFirstName()); + } + if (removedUser.getLastName() != null) { + metaData.putValue("userLastName", removedUser.getLastName()); + } + } + protected abstract void setCustomerId(TenantId tenantId, CustomerId customerId, User user, UserUpdateMsg userUpdateMsg); } diff --git a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java index 968d573618..3b2d76356b 100644 --- a/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java +++ b/application/src/main/java/org/thingsboard/server/service/edge/rpc/processor/user/UserEdgeProcessor.java @@ -120,17 +120,6 @@ public class UserEdgeProcessor extends BaseUserProcessor implements UserProcesso } } - private void deleteUserAndPushEntityDeletedEventToRuleEngine(TenantId tenantId, UserId userId, Edge edge) { - User removedUser = deleteUser(tenantId, userId); - if (removedUser == null) { - return; - } - CustomerId userCustomerId = removedUser.getCustomerId(); - String userAsString = JacksonUtil.toString(removedUser); - TbMsgMetaData msgMetaData = getEdgeActionTbMsgMetaData(edge, userCustomerId); - pushEntityEventToRuleEngine(tenantId, userId, userCustomerId, TbMsgType.ENTITY_DELETED, userAsString, msgMetaData); - } - @Override public DownlinkMsg convertEdgeEventToDownlink(EdgeEvent edgeEvent, EdgeVersion edgeVersion) { UserId userId = new UserId(edgeEvent.getEntityId());