From ee14e5d7d169117c6678918fb9318caadef9becc Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Thu, 26 Mar 2026 13:01:08 +0100 Subject: [PATCH] Enhance auth user lifecycle and registration checks --- .../auth/config/JwtAuthenticationFilter.java | 2 +- .../manager/auth/model/AuthRequest.java | 10 ++ .../manager/auth/repo/AuthRepository.java | 17 ++ .../manager/auth/repo/LoginEntity.java | 13 +- .../manager/auth/services/AuthService.java | 34 ++-- .../auth/services/TurnstileService.java | 65 +++++++ .../auth/services/UserImportComponent.java | 3 + .../manager/controllers/AuthController.java | 7 + .../executions/repo/ExecutionRepository.java | 4 + .../manager/flows/repo/FlowRepository.java | 4 + src/main/resources/application.properties | 3 + .../resources/workflow-editor-init/users.json | 16 +- .../controllers/AuthControllerTest.java | 160 +++++++++++++++--- src/test/resources/test.properties | 1 + 14 files changed, 295 insertions(+), 44 deletions(-) create mode 100644 src/main/java/it/cnr/isti/workflow/manager/auth/services/TurnstileService.java diff --git a/src/main/java/it/cnr/isti/workflow/manager/auth/config/JwtAuthenticationFilter.java b/src/main/java/it/cnr/isti/workflow/manager/auth/config/JwtAuthenticationFilter.java index 2ba24ff..941d37e 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/auth/config/JwtAuthenticationFilter.java +++ b/src/main/java/it/cnr/isti/workflow/manager/auth/config/JwtAuthenticationFilter.java @@ -58,7 +58,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter { String username = jwtUtil.extractUsername(jwt); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { - LoginEntity userDetails = authRepository.findById(username).orElse(null); + LoginEntity userDetails = authRepository.findByUsernameAndActiveTrue(username).orElse(null); if (userDetails == null) { logger.warn("JWT username '{}' not found in database", username); } else if (jwtUtil.validateToken(jwt, userDetails)) { diff --git a/src/main/java/it/cnr/isti/workflow/manager/auth/model/AuthRequest.java b/src/main/java/it/cnr/isti/workflow/manager/auth/model/AuthRequest.java index 8092624..84d634e 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/auth/model/AuthRequest.java +++ b/src/main/java/it/cnr/isti/workflow/manager/auth/model/AuthRequest.java @@ -9,6 +9,13 @@ public class AuthRequest { private String username; private String password; private String email; + private String turnstileToken; + + public AuthRequest(String username, String password, String email) { + this.username = username; + this.password = password; + this.email = email; + } // Getters e Setters public String getUsername() { return username; } @@ -20,6 +27,9 @@ public class AuthRequest { public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } + public String getTurnstileToken() { return turnstileToken; } + public void setTurnstileToken(String turnstileToken) { this.turnstileToken = turnstileToken; } + boolean isValid() { return username.contains(" ") && username != null && !username.isEmpty() && username.length()>5 && password != null && username.contains(" ") && password.length()>5 && !password.isEmpty(); diff --git a/src/main/java/it/cnr/isti/workflow/manager/auth/repo/AuthRepository.java b/src/main/java/it/cnr/isti/workflow/manager/auth/repo/AuthRepository.java index 9225d2d..f655e70 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/auth/repo/AuthRepository.java +++ b/src/main/java/it/cnr/isti/workflow/manager/auth/repo/AuthRepository.java @@ -2,13 +2,30 @@ package it.cnr.isti.workflow.manager.auth.repo; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; import it.cnr.isti.workflow.manager.auth.model.UserRole; +import java.util.List; +import java.util.Optional; + @Repository public interface AuthRepository extends JpaRepository { long countByRole(UserRole role); + @Query("select count(u) from LoginEntity u where u.role = :role and (u.active is null or u.active = true)") + long countByRoleAndActiveTrue(@Param("role") UserRole role); + + @Query("select case when count(u) > 0 then true else false end from LoginEntity u where u.username = :username and (u.active is null or u.active = true)") + boolean existsByUsernameAndActiveTrue(@Param("username") String username); + + @Query("select u from LoginEntity u where u.username = :username and (u.active is null or u.active = true)") + Optional findByUsernameAndActiveTrue(@Param("username") String username); + + @Query("select u from LoginEntity u where u.active is null or u.active = true") + List findByActiveTrue(); + } diff --git a/src/main/java/it/cnr/isti/workflow/manager/auth/repo/LoginEntity.java b/src/main/java/it/cnr/isti/workflow/manager/auth/repo/LoginEntity.java index 432043f..c2ff0dc 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/auth/repo/LoginEntity.java +++ b/src/main/java/it/cnr/isti/workflow/manager/auth/repo/LoginEntity.java @@ -18,18 +18,27 @@ public class LoginEntity { private String username; private String password; private String email; + private Boolean active; @Enumerated(EnumType.STRING) private UserRole role; public LoginEntity(String username, String password) { - this(username, password, null, UserRole.USER); + this(username, password, null, true, UserRole.USER); } public LoginEntity(String username, String password, UserRole role) { - this(username, password, null, role); + this(username, password, null, true, role); + } + + public LoginEntity(String username, String password, String email, UserRole role) { + this(username, password, email, true, role); } public UserRole effectiveRole() { return role == null ? UserRole.USER : role; } + + public boolean isActiveUser() { + return active == null || active; + } } diff --git a/src/main/java/it/cnr/isti/workflow/manager/auth/services/AuthService.java b/src/main/java/it/cnr/isti/workflow/manager/auth/services/AuthService.java index 7d0c03a..c6730fa 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/auth/services/AuthService.java +++ b/src/main/java/it/cnr/isti/workflow/manager/auth/services/AuthService.java @@ -3,6 +3,7 @@ package it.cnr.isti.workflow.manager.auth.services; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import it.cnr.isti.workflow.manager.auth.model.AdminChangePasswordResult; import it.cnr.isti.workflow.manager.auth.model.ChangePasswordResult; @@ -13,6 +14,8 @@ import it.cnr.isti.workflow.manager.auth.model.UserRole; import it.cnr.isti.workflow.manager.auth.model.UserView; import it.cnr.isti.workflow.manager.auth.repo.AuthRepository; import it.cnr.isti.workflow.manager.auth.repo.LoginEntity; +import it.cnr.isti.workflow.manager.executions.repo.ExecutionRepository; +import it.cnr.isti.workflow.manager.flows.repo.FlowRepository; import static it.cnr.isti.workflow.manager.auth.services.PasswordHasher.*; @@ -23,8 +26,14 @@ public class AuthService { @Autowired AuthRepository authRepository; + @Autowired + FlowRepository flowRepository; + + @Autowired + ExecutionRepository executionRepository; + public boolean validateUser(String username, String password) throws UsernameNotFoundException { - LoginEntity le = authRepository.findById(username).orElse(null); + LoginEntity le = authRepository.findByUsernameAndActiveTrue(username).orElse(null); if (le == null) { throw new UsernameNotFoundException("User not found"); } @@ -52,7 +61,7 @@ public class AuthService { } public ChangePasswordResult changePassword(String username, String oldPassword, String newPassword) { - LoginEntity user = authRepository.findById(username).orElse(null); + LoginEntity user = authRepository.findByUsernameAndActiveTrue(username).orElse(null); if (user == null) { return ChangePasswordResult.USER_NOT_FOUND; } @@ -68,7 +77,7 @@ public class AuthService { } public AdminChangePasswordResult adminChangePassword(String username, String newPassword) { - LoginEntity user = authRepository.findById(username).orElse(null); + LoginEntity user = authRepository.findByUsernameAndActiveTrue(username).orElse(null); if (user == null) { return AdminChangePasswordResult.USER_NOT_FOUND; } @@ -80,20 +89,25 @@ public class AuthService { return AdminChangePasswordResult.SUCCESS; } + @Transactional public DeleteUserResult deleteUser(String username) { - LoginEntity user = authRepository.findById(username).orElse(null); + LoginEntity user = authRepository.findByUsernameAndActiveTrue(username).orElse(null); if (user == null) { return DeleteUserResult.USER_NOT_FOUND; } - if (user.effectiveRole() == UserRole.ADMIN && authRepository.countByRole(UserRole.ADMIN) <= 1) { + if (user.effectiveRole() == UserRole.ADMIN && authRepository.countByRoleAndActiveTrue(UserRole.ADMIN) <= 1) { return DeleteUserResult.LAST_ADMIN; } - authRepository.deleteById(username); + + executionRepository.deleteByOwner(username); + flowRepository.deleteAll(flowRepository.findByOwnerAndFinalizedFalse(username)); + user.setActive(false); + authRepository.save(user); return DeleteUserResult.SUCCESS; } public ChangeUserRoleResult changeUserRole(String username, UserRole role) { - LoginEntity user = authRepository.findById(username).orElse(null); + LoginEntity user = authRepository.findByUsernameAndActiveTrue(username).orElse(null); if (user == null) { return ChangeUserRoleResult.USER_NOT_FOUND; } @@ -102,7 +116,7 @@ public class AuthService { } if (user.effectiveRole() == UserRole.ADMIN && role != UserRole.ADMIN - && authRepository.countByRole(UserRole.ADMIN) <= 1) { + && authRepository.countByRoleAndActiveTrue(UserRole.ADMIN) <= 1) { return ChangeUserRoleResult.LAST_ADMIN; } user.setRole(role); @@ -111,14 +125,14 @@ public class AuthService { } public List listUsers() { - return authRepository.findAll().stream() + return authRepository.findByActiveTrue().stream() .map(user -> new UserView(user.getUsername(), user.getEmail(), user.effectiveRole())) .sorted(java.util.Comparator.comparing(UserView::username)) .toList(); } public LoginEntity getUser(String username) { - return authRepository.findById(username).orElse(null); + return authRepository.findByUsernameAndActiveTrue(username).orElse(null); } private boolean isValidPassword(String password) { diff --git a/src/main/java/it/cnr/isti/workflow/manager/auth/services/TurnstileService.java b/src/main/java/it/cnr/isti/workflow/manager/auth/services/TurnstileService.java new file mode 100644 index 0000000..e747ebc --- /dev/null +++ b/src/main/java/it/cnr/isti/workflow/manager/auth/services/TurnstileService.java @@ -0,0 +1,65 @@ +package it.cnr.isti.workflow.manager.auth.services; + +import java.util.Objects; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.web.reactive.function.BodyInserters; +import org.springframework.web.reactive.function.client.WebClient; + +@Service +public class TurnstileService { + + private static final Logger logger = LoggerFactory.getLogger(TurnstileService.class); + + private final WebClient.Builder webClientBuilder; + private final boolean enabled; + private final String secret; + private final String verifyUrl; + + public TurnstileService(WebClient.Builder webClientBuilder, + @Value("${app.turnstile.enabled:false}") boolean enabled, + @Value("${app.turnstile.secret:}") String secret, + @Value("${app.turnstile.verify-url:https://challenges.cloudflare.com/turnstile/v0/siteverify}") String verifyUrl) { + this.webClientBuilder = Objects.requireNonNull(webClientBuilder, "webClientBuilder cannot be null"); + this.enabled = enabled; + this.secret = secret; + this.verifyUrl = verifyUrl; + } + + public boolean verifyRegistrationToken(String token) { + if (!enabled) { + return true; + } + if (!StringUtils.hasText(secret)) { + logger.error("Turnstile validation is enabled but no secret is configured"); + return false; + } + if (!StringUtils.hasText(token)) { + return false; + } + + try { + TurnstileVerificationResponse response = webClientBuilder.build() + .post() + .uri(verifyUrl) + .contentType(MediaType.APPLICATION_FORM_URLENCODED) + .body(BodyInserters.fromFormData("secret", secret) + .with("response", token)) + .retrieve() + .bodyToMono(TurnstileVerificationResponse.class) + .block(); + return response != null && Boolean.TRUE.equals(response.success()); + } catch (RuntimeException exception) { + logger.warn("Turnstile verification failed", exception); + return false; + } + } + + private record TurnstileVerificationResponse(Boolean success) { + } +} diff --git a/src/main/java/it/cnr/isti/workflow/manager/auth/services/UserImportComponent.java b/src/main/java/it/cnr/isti/workflow/manager/auth/services/UserImportComponent.java index ad30c82..46aa21a 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/auth/services/UserImportComponent.java +++ b/src/main/java/it/cnr/isti/workflow/manager/auth/services/UserImportComponent.java @@ -52,6 +52,9 @@ public class UserImportComponent { user.getUsername()), () -> { logger.debug("User {} not found, saving new user", user.getUsername()); + if (user.getActive() == null) { + user.setActive(true); + } user.setPassword(PasswordHasher.hashPassword(user.getPassword())); authRepository.save(user); }); diff --git a/src/main/java/it/cnr/isti/workflow/manager/controllers/AuthController.java b/src/main/java/it/cnr/isti/workflow/manager/controllers/AuthController.java index 358b018..42e2ed0 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/controllers/AuthController.java +++ b/src/main/java/it/cnr/isti/workflow/manager/controllers/AuthController.java @@ -15,6 +15,7 @@ import it.cnr.isti.workflow.manager.auth.model.ChangePasswordRequest; import it.cnr.isti.workflow.manager.auth.model.CreateUserResult; import it.cnr.isti.workflow.manager.auth.model.DeleteUserResult; import it.cnr.isti.workflow.manager.auth.services.AuthService; +import it.cnr.isti.workflow.manager.auth.services.TurnstileService; import org.slf4j.Logger; import org.springframework.beans.factory.annotation.Autowired; @@ -45,6 +46,9 @@ public class AuthController { @Autowired private AuthService authService; + @Autowired + private TurnstileService turnstileService; + @PostMapping("/login") @Operation(summary = "Login", description = "Authenticates a user and returns a JWT token on success.") public ResponseEntity login(@RequestBody AuthRequest request) { @@ -76,6 +80,9 @@ public class AuthController { if (request.getUsername() == null || request.getPassword() == null || request.getEmail() == null) { return ResponseEntity.badRequest().body("Username, password and email are required"); } + if (!turnstileService.verifyRegistrationToken(request.getTurnstileToken())) { + return ResponseEntity.badRequest().body("INVALID_TURNSTILE"); + } if (request.getUsername().length() < 3) { return ResponseEntity.badRequest() .body("Username must be at least 3 characters"); diff --git a/src/main/java/it/cnr/isti/workflow/manager/executions/repo/ExecutionRepository.java b/src/main/java/it/cnr/isti/workflow/manager/executions/repo/ExecutionRepository.java index 1b0fa7b..886fab4 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/executions/repo/ExecutionRepository.java +++ b/src/main/java/it/cnr/isti/workflow/manager/executions/repo/ExecutionRepository.java @@ -2,5 +2,9 @@ package it.cnr.isti.workflow.manager.executions.repo; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface ExecutionRepository extends JpaRepository { + List findByOwner(String owner); + void deleteByOwner(String owner); } diff --git a/src/main/java/it/cnr/isti/workflow/manager/flows/repo/FlowRepository.java b/src/main/java/it/cnr/isti/workflow/manager/flows/repo/FlowRepository.java index e6338e2..0d6ab5d 100644 --- a/src/main/java/it/cnr/isti/workflow/manager/flows/repo/FlowRepository.java +++ b/src/main/java/it/cnr/isti/workflow/manager/flows/repo/FlowRepository.java @@ -15,4 +15,8 @@ public interface FlowRepository extends JpaRepository { List findFlowsByOwnerOrPublic(@Param("owner") String owner); java.util.Optional findByOwnerAndName(String owner, String name); + + List findByOwner(String owner); + + List findByOwnerAndFinalizedFalse(String owner); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bc6f6ea..7b28bfa 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -37,5 +37,8 @@ app.assistant.default-model=${ASSISTANT_DEFAULT_MODEL:gemma3:12b} cors.allowed-origins=${CORS_ALLOWED_ORIGINS:http://localhost:4200} app.import.path=${IMPORT_PATH:/workflow-editor-init} app.import.enabled=true +app.turnstile.enabled=${TURNSTILE_ENABLED:false} +app.turnstile.secret=${TURNSTILE_SECRET:} +app.turnstile.verify-url=${TURNSTILE_VERIFY_URL:https://challenges.cloudflare.com/turnstile/v0/siteverify} logging.level.it.cnr.isti.workflow.manager=DEBUG logging.level.root=ERROR diff --git a/src/main/resources/workflow-editor-init/users.json b/src/main/resources/workflow-editor-init/users.json index 16e89c7..bccca9f 100644 --- a/src/main/resources/workflow-editor-init/users.json +++ b/src/main/resources/workflow-editor-init/users.json @@ -1,7 +1,13 @@ [ - { - "username": "testuser", - "password": "testpassword", - "email": "testuser@example.com" - } + { + "username": "testuser", + "password": "testpassword", + "email": "testuser@example.com" + }, + { + "username": "admin", + "password": "Adminpass1!", + "email": "admin@example.com", + "role": "ADMIN" + } ] diff --git a/src/test/java/it/cnr/isti/workflow/manager/controllers/AuthControllerTest.java b/src/test/java/it/cnr/isti/workflow/manager/controllers/AuthControllerTest.java index 87c2025..e980399 100644 --- a/src/test/java/it/cnr/isti/workflow/manager/controllers/AuthControllerTest.java +++ b/src/test/java/it/cnr/isti/workflow/manager/controllers/AuthControllerTest.java @@ -3,7 +3,9 @@ package it.cnr.isti.workflow.manager.controllers; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; @@ -12,6 +14,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.TestPropertySource; import org.springframework.test.web.servlet.MockMvc; @@ -24,24 +27,50 @@ import it.cnr.isti.workflow.manager.auth.model.ChangePasswordRequest; import it.cnr.isti.workflow.manager.auth.model.UserRole; import it.cnr.isti.workflow.manager.auth.repo.AuthRepository; import it.cnr.isti.workflow.manager.auth.repo.LoginEntity; +import it.cnr.isti.workflow.manager.auth.services.TurnstileService; +import it.cnr.isti.workflow.manager.executions.persistence.ExecutionSnapshot; +import it.cnr.isti.workflow.manager.executions.repo.ExecutionEntity; +import it.cnr.isti.workflow.manager.executions.repo.ExecutionRepository; +import it.cnr.isti.workflow.manager.executions.ExecutionStatus; +import it.cnr.isti.workflow.manager.flows.model.FlowData; +import it.cnr.isti.workflow.manager.flows.repo.FlowEntity; +import it.cnr.isti.workflow.manager.flows.repo.FlowRepository; @SpringBootTest @AutoConfigureMockMvc @TestPropertySource(locations = "classpath:test.properties") public class AuthControllerTest { + private static String uniqueSuffix() { + return java.util.UUID.randomUUID().toString().substring(0, 8); + } + @Autowired private AuthController authController; @Autowired private AuthRepository authRepository; + @Autowired + private FlowRepository flowRepository; + + @Autowired + private ExecutionRepository executionRepository; + @Autowired private MockMvc mockMvc; @Autowired private JwtUtil jwtUtil; + @MockitoBean + private TurnstileService turnstileService; + + @BeforeEach + public void configureTurnstile() { + Mockito.when(turnstileService.verifyRegistrationToken(Mockito.any())).thenReturn(true); + } + @Test public void testLogin() { ResponseEntity response = authController.login(new AuthRequest("testuser", "testpassword", "testuser@example.com")); @@ -53,22 +82,32 @@ public class AuthControllerTest { @Test public void testChangePassword() { + String suffix = uniqueSuffix(); + String username = "changepwd-" + suffix; + String oldPassword = "Startpass1!"; + String newPassword = "Newpassword1!"; + authController.register(new AuthRequest(username, oldPassword, username + "@example.com")); + ChangePasswordRequest request = new ChangePasswordRequest(); - request.setUsername("testuser"); - request.setOldPassword("testpassword"); - request.setNewPassword("Newpassword1!"); + request.setUsername(username); + request.setOldPassword(oldPassword); + request.setNewPassword(newPassword); ResponseEntity response = authController.changePassword(request); assert response.getStatusCode().is2xxSuccessful(); - ResponseEntity loginResponse = authController.login(new AuthRequest("testuser", "Newpassword1!", "testuser@example.com")); + ResponseEntity loginResponse = authController.login(new AuthRequest(username, newPassword, username + "@example.com")); assert loginResponse.getStatusCode().is2xxSuccessful(); } @Test public void testChangePasswordReturnsUnauthorizedForInvalidCurrentPassword() { + String suffix = uniqueSuffix(); + String username = "wrongold-" + suffix; + authController.register(new AuthRequest(username, "Startpass1!", username + "@example.com")); + ChangePasswordRequest request = new ChangePasswordRequest(); - request.setUsername("testuser"); + request.setUsername(username); request.setOldPassword("wrong-password"); request.setNewPassword("Anotherpass1!"); @@ -78,10 +117,12 @@ public class AuthControllerTest { @Test public void testChangePasswordReturnsInvalidNewPasswordForWeakPassword() { - authController.register(new AuthRequest("weakpwduser", "Startpass1!", "weakpwduser@example.com")); + String suffix = uniqueSuffix(); + String username = "weakpwduser-" + suffix; + authController.register(new AuthRequest(username, "Startpass1!", username + "@example.com")); ChangePasswordRequest request = new ChangePasswordRequest(); - request.setUsername("weakpwduser"); + request.setUsername(username); request.setOldPassword("Startpass1!"); request.setNewPassword("weakpass"); @@ -90,11 +131,28 @@ public class AuthControllerTest { assert "INVALID_NEW_PASSWORD".equals(response.getBody()); } + @Test + public void registerRejectsInvalidTurnstile() { + Mockito.when(turnstileService.verifyRegistrationToken(Mockito.any())).thenReturn(false); + + ResponseEntity response = authController.register( + new AuthRequest("turnstile-user-" + uniqueSuffix(), "Validpass1!", "turnstile-user@example.com")); + + assert response.getStatusCode() == HttpStatus.BAD_REQUEST; + assert "INVALID_TURNSTILE".equals(response.getBody()); + } + @Test public void adminCanCreateListChangePasswordAndDeleteUsers() { - LoginEntity admin = new LoginEntity("adminuser", + String suffix = uniqueSuffix(); + String adminUsername = "adminuser-" + suffix; + String managedUsername = "manageduser-" + suffix; + String managedEmail = managedUsername + "@example.com"; + String executionId = "managed-exec-" + suffix; + + LoginEntity admin = new LoginEntity(adminUsername, it.cnr.isti.workflow.manager.auth.services.PasswordHasher.hashPassword("Adminpass1!"), - "adminuser@example.com", + adminUsername + "@example.com", UserRole.ADMIN); authRepository.save(admin); @@ -104,34 +162,81 @@ public class AuthControllerTest { java.util.List.of(new SimpleGrantedAuthority("ROLE_ADMIN")))); try { ResponseEntity createResponse = authController.createUser( - new AdminCreateUserRequest("manageduser", "Managedpass1!", "manageduser@example.com", UserRole.USER)); + new AdminCreateUserRequest(managedUsername, "Managedpass1!", managedEmail, UserRole.USER)); assert createResponse.getStatusCode().is2xxSuccessful(); ResponseEntity listResponse = authController.listUsers(); assert listResponse.getStatusCode().is2xxSuccessful(); assert listResponse.getBody() instanceof java.util.List; assert ((java.util.List) listResponse.getBody()).stream() - .anyMatch(item -> item.toString().contains("manageduser") && item.toString().contains("manageduser@example.com")); + .anyMatch(item -> item.toString().contains(managedUsername) && item.toString().contains(managedEmail)); ResponseEntity passwordResponse = authController.adminChangePassword( - "manageduser", + managedUsername, new AdminChangeUserPasswordRequest("Changedpass1!")); assert passwordResponse.getStatusCode().is2xxSuccessful(); ResponseEntity roleResponse = authController.adminChangeUserRole( - "manageduser", + managedUsername, new AdminChangeUserRoleRequest(UserRole.ADMIN)); assert roleResponse.getStatusCode().is2xxSuccessful(); - assert authRepository.findById("manageduser").orElseThrow().effectiveRole() == UserRole.ADMIN; + assert authRepository.findById(managedUsername).orElseThrow().effectiveRole() == UserRole.ADMIN; - ResponseEntity loginResponse = authController.login(new AuthRequest("manageduser", "Changedpass1!", "manageduser@example.com")); + ResponseEntity loginResponse = authController.login(new AuthRequest(managedUsername, "Changedpass1!", managedEmail)); assert loginResponse.getStatusCode().is2xxSuccessful(); assert loginResponse.getBody() instanceof java.util.Map; assert "ADMIN".equals(((java.util.Map) loginResponse.getBody()).get("role")); - assert "manageduser@example.com".equals(((java.util.Map) loginResponse.getBody()).get("email")); + assert managedEmail.equals(((java.util.Map) loginResponse.getBody()).get("email")); - ResponseEntity deleteResponse = authController.deleteUser("manageduser"); + FlowEntity draftFlow = flowRepository.save(FlowEntity.builder() + .name("Managed Draft") + .owner(managedUsername) + .description("draft") + .published(true) + .finalized(false) + .createdAt(java.time.LocalDateTime.now().minusDays(1)) + .lastUpdateAt(java.time.LocalDateTime.now().minusHours(1)) + .flow(FlowData.builder().build()) + .build()); + FlowEntity finalizedFlow = flowRepository.save(FlowEntity.builder() + .name("Managed Final") + .owner(managedUsername) + .description("final") + .published(true) + .finalized(true) + .createdAt(java.time.LocalDateTime.now().minusDays(2)) + .lastUpdateAt(java.time.LocalDateTime.now().minusHours(2)) + .flow(FlowData.builder().build()) + .build()); + executionRepository.save(ExecutionEntity.builder() + .id(executionId) + .name("Managed Execution") + .owner(managedUsername) + .creationTime(100L) + .lastUpdateTime(200L) + .flow(FlowData.builder().build()) + .snapshot(ExecutionSnapshot.builder() + .status(ExecutionStatus.SUCCESS) + .providedAuthorizations(java.util.Map.of()) + .build()) + .build()); + + ResponseEntity deleteResponse = authController.deleteUser(managedUsername); assert deleteResponse.getStatusCode().is2xxSuccessful(); + + ResponseEntity deletedLoginResponse = authController.login( + new AuthRequest(managedUsername, "Changedpass1!", managedEmail)); + assert deletedLoginResponse.getStatusCode() == HttpStatus.NOT_FOUND; + + ResponseEntity listAfterDelete = authController.listUsers(); + assert listAfterDelete.getStatusCode().is2xxSuccessful(); + assert listAfterDelete.getBody() instanceof java.util.List; + assert ((java.util.List) listAfterDelete.getBody()).stream() + .noneMatch(item -> item.toString().contains(managedUsername)); + + assert executionRepository.findByOwner(managedUsername).isEmpty(); + assert flowRepository.findById(draftFlow.getId()).isEmpty(); + assert flowRepository.findById(finalizedFlow.getId()).isPresent(); } finally { SecurityContextHolder.clearContext(); } @@ -143,9 +248,10 @@ public class AuthControllerTest { .filter(user -> user.effectiveRole() == UserRole.ADMIN) .map(LoginEntity::getUsername) .forEach(authRepository::deleteById); - LoginEntity admin = new LoginEntity("soloadmin", + String username = "soloadmin-" + uniqueSuffix(); + LoginEntity admin = new LoginEntity(username, it.cnr.isti.workflow.manager.auth.services.PasswordHasher.hashPassword("Adminpass1!"), - "soloadmin@example.com", + username + "@example.com", UserRole.ADMIN); authRepository.save(admin); @@ -155,7 +261,7 @@ public class AuthControllerTest { java.util.List.of(new SimpleGrantedAuthority("ROLE_ADMIN")))); try { ResponseEntity roleResponse = authController.adminChangeUserRole( - "soloadmin", + username, new AdminChangeUserRoleRequest(UserRole.USER)); assert roleResponse.getStatusCode() == HttpStatus.CONFLICT; assert "LAST_ADMIN".equals(roleResponse.getBody()); @@ -170,9 +276,10 @@ public class AuthControllerTest { .filter(user -> user.effectiveRole() == UserRole.ADMIN) .map(LoginEntity::getUsername) .forEach(authRepository::deleteById); - LoginEntity admin = new LoginEntity("onlyadmin", + String username = "onlyadmin-" + uniqueSuffix(); + LoginEntity admin = new LoginEntity(username, it.cnr.isti.workflow.manager.auth.services.PasswordHasher.hashPassword("Adminpass1!"), - "onlyadmin@example.com", + username + "@example.com", UserRole.ADMIN); authRepository.save(admin); @@ -181,7 +288,7 @@ public class AuthControllerTest { null, java.util.List.of(new SimpleGrantedAuthority("ROLE_ADMIN")))); try { - ResponseEntity deleteResponse = authController.deleteUser("onlyadmin"); + ResponseEntity deleteResponse = authController.deleteUser(username); assert deleteResponse.getStatusCode() == HttpStatus.CONFLICT; assert "LAST_ADMIN".equals(deleteResponse.getBody()); } finally { @@ -191,16 +298,17 @@ public class AuthControllerTest { @Test public void registerRejectsInvalidEmail() { - ResponseEntity response = authController.register(new AuthRequest("mailuser", "Validpass1!", "not-an-email")); + ResponseEntity response = authController.register(new AuthRequest("mailuser-" + uniqueSuffix(), "Validpass1!", "not-an-email")); assert response.getStatusCode() == HttpStatus.BAD_REQUEST; assert "INVALID_EMAIL".equals(response.getBody()); } @Test public void adminEndpointsAcceptJwtAdminRole() throws Exception { - LoginEntity admin = new LoginEntity("jwtadmin", + String username = "jwtadmin-" + uniqueSuffix(); + LoginEntity admin = new LoginEntity(username, it.cnr.isti.workflow.manager.auth.services.PasswordHasher.hashPassword("Adminpass1!"), - "jwtadmin@example.com", + username + "@example.com", UserRole.ADMIN); authRepository.save(admin); diff --git a/src/test/resources/test.properties b/src/test/resources/test.properties index 0bbcae2..f40cf02 100644 --- a/src/test/resources/test.properties +++ b/src/test/resources/test.properties @@ -9,6 +9,7 @@ spring.jpa.properties.jakarta.persistence.validation.mode=none app.db.init.enabled=true app.assistant.default-model=assistant-test-model app.mcp.bridge.url=http://localhost:18080 +app.turnstile.enabled=false app.import.path=src/test/resources/workflow-editor-init app.import.enabled=true