users administration api added

This commit is contained in:
Lucio Lelii 2026-03-26 09:58:33 +01:00
parent 99472ffec4
commit 7d7b131f55
23 changed files with 485 additions and 42 deletions

View File

@ -1,12 +1,12 @@
package it.cnr.isti.workflow.manager.auth.config;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.NonNull;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
@ -30,7 +30,13 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private AuthRepository authRepository;
private static final List<String> EXCLUDED_PATHS = List.of("/auth/", "/health", "/blocks/types", "/retriever/");
private static final List<String> EXCLUDED_PATHS = List.of(
"/auth/login",
"/auth/register",
"/auth/change-password",
"/health",
"/blocks/types",
"/retriever/");
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@ -57,7 +63,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
logger.warn("JWT username '{}' not found in database", username);
} else if (jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(userDetails,
null, Collections.emptyList());
null, List.of(new SimpleGrantedAuthority("ROLE_" + userDetails.effectiveRole().name())));
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authToken);

View File

@ -28,7 +28,9 @@ public class SecurityConfig {
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/auth/login").permitAll()
.requestMatchers("/auth/register").permitAll()
.requestMatchers("/auth/change-password").permitAll()
.requestMatchers("/swagger-ui.html").permitAll()
.requestMatchers("/swagger-ui/**").permitAll()
.requestMatchers("/v3/api-docs/**").permitAll()

View File

@ -0,0 +1,7 @@
package it.cnr.isti.workflow.manager.auth.model;
public enum AdminChangePasswordResult {
SUCCESS,
USER_NOT_FOUND,
INVALID_PASSWORD
}

View File

@ -0,0 +1,5 @@
package it.cnr.isti.workflow.manager.auth.model;
public record AdminChangeUserPasswordRequest(
String newPassword) {
}

View File

@ -0,0 +1,5 @@
package it.cnr.isti.workflow.manager.auth.model;
public record AdminChangeUserRoleRequest(
UserRole role) {
}

View File

@ -0,0 +1,8 @@
package it.cnr.isti.workflow.manager.auth.model;
public record AdminCreateUserRequest(
String username,
String password,
String email,
UserRole role) {
}

View File

@ -8,6 +8,7 @@ import lombok.NoArgsConstructor;
public class AuthRequest {
private String username;
private String password;
private String email;
// Getters e Setters
public String getUsername() { return username; }
@ -16,9 +17,11 @@ public class AuthRequest {
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
boolean isValid() {
return username.contains(" ") && username != null && !username.isEmpty() && username.length()>5 &&
password != null && username.contains(" ") && password.length()>5 && !password.isEmpty();
}
}

View File

@ -0,0 +1,8 @@
package it.cnr.isti.workflow.manager.auth.model;
public enum ChangePasswordResult {
SUCCESS,
USER_NOT_FOUND,
INVALID_OLD_PASSWORD,
INVALID_NEW_PASSWORD
}

View File

@ -0,0 +1,8 @@
package it.cnr.isti.workflow.manager.auth.model;
public enum ChangeUserRoleResult {
SUCCESS,
USER_NOT_FOUND,
INVALID_ROLE,
LAST_ADMIN
}

View File

@ -0,0 +1,8 @@
package it.cnr.isti.workflow.manager.auth.model;
public enum CreateUserResult {
SUCCESS,
USER_ALREADY_EXISTS,
INVALID_EMAIL,
INVALID_PASSWORD
}

View File

@ -0,0 +1,7 @@
package it.cnr.isti.workflow.manager.auth.model;
public enum DeleteUserResult {
SUCCESS,
USER_NOT_FOUND,
LAST_ADMIN
}

View File

@ -0,0 +1,6 @@
package it.cnr.isti.workflow.manager.auth.model;
public enum UserRole {
USER,
ADMIN
}

View File

@ -0,0 +1,7 @@
package it.cnr.isti.workflow.manager.auth.model;
public record UserView(
String username,
String email,
UserRole role) {
}

View File

@ -4,8 +4,11 @@ package it.cnr.isti.workflow.manager.auth.repo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import it.cnr.isti.workflow.manager.auth.model.UserRole;
@Repository
public interface AuthRepository extends JpaRepository<LoginEntity, String> {
long countByRole(UserRole role);
}

View File

@ -1,6 +1,9 @@
package it.cnr.isti.workflow.manager.auth.repo;
import it.cnr.isti.workflow.manager.auth.model.UserRole;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
@ -14,4 +17,19 @@ public class LoginEntity {
@Id
private String username;
private String password;
private String email;
@Enumerated(EnumType.STRING)
private UserRole role;
public LoginEntity(String username, String password) {
this(username, password, null, UserRole.USER);
}
public LoginEntity(String username, String password, UserRole role) {
this(username, password, null, role);
}
public UserRole effectiveRole() {
return role == null ? UserRole.USER : role;
}
}

View File

@ -4,20 +4,22 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import it.cnr.isti.workflow.manager.auth.model.AdminChangePasswordResult;
import it.cnr.isti.workflow.manager.auth.model.ChangePasswordResult;
import it.cnr.isti.workflow.manager.auth.model.ChangeUserRoleResult;
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.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 static it.cnr.isti.workflow.manager.auth.services.PasswordHasher.*;
import java.util.List;
@Service
public class AuthService {
public enum ChangePasswordResult {
SUCCESS,
USER_NOT_FOUND,
INVALID_OLD_PASSWORD,
INVALID_NEW_PASSWORD
}
@Autowired
AuthRepository authRepository;
@ -30,13 +32,23 @@ public class AuthService {
}
public boolean registerUser(String username, String password) {
public CreateUserResult registerUser(String username, String password, String email) {
return createUser(username, password, email, UserRole.USER);
}
public CreateUserResult createUser(String username, String password, String email, UserRole role) {
if (authRepository.existsById(username)) {
return false; // User already exists
return CreateUserResult.USER_ALREADY_EXISTS;
}
if (!isValidEmail(email)) {
return CreateUserResult.INVALID_EMAIL;
}
if (!isValidPassword(password)) {
return CreateUserResult.INVALID_PASSWORD;
}
String hashedPassword = hashPassword(password);
authRepository.save(new LoginEntity(username, hashedPassword));
return true;
authRepository.save(new LoginEntity(username, hashedPassword, email.trim(), role == null ? UserRole.USER : role));
return CreateUserResult.SUCCESS;
}
public ChangePasswordResult changePassword(String username, String oldPassword, String newPassword) {
@ -55,6 +67,60 @@ public class AuthService {
return ChangePasswordResult.SUCCESS;
}
public AdminChangePasswordResult adminChangePassword(String username, String newPassword) {
LoginEntity user = authRepository.findById(username).orElse(null);
if (user == null) {
return AdminChangePasswordResult.USER_NOT_FOUND;
}
if (!isValidPassword(newPassword)) {
return AdminChangePasswordResult.INVALID_PASSWORD;
}
user.setPassword(hashPassword(newPassword));
authRepository.save(user);
return AdminChangePasswordResult.SUCCESS;
}
public DeleteUserResult deleteUser(String username) {
LoginEntity user = authRepository.findById(username).orElse(null);
if (user == null) {
return DeleteUserResult.USER_NOT_FOUND;
}
if (user.effectiveRole() == UserRole.ADMIN && authRepository.countByRole(UserRole.ADMIN) <= 1) {
return DeleteUserResult.LAST_ADMIN;
}
authRepository.deleteById(username);
return DeleteUserResult.SUCCESS;
}
public ChangeUserRoleResult changeUserRole(String username, UserRole role) {
LoginEntity user = authRepository.findById(username).orElse(null);
if (user == null) {
return ChangeUserRoleResult.USER_NOT_FOUND;
}
if (role == null) {
return ChangeUserRoleResult.INVALID_ROLE;
}
if (user.effectiveRole() == UserRole.ADMIN
&& role != UserRole.ADMIN
&& authRepository.countByRole(UserRole.ADMIN) <= 1) {
return ChangeUserRoleResult.LAST_ADMIN;
}
user.setRole(role);
authRepository.save(user);
return ChangeUserRoleResult.SUCCESS;
}
public List<UserView> listUsers() {
return authRepository.findAll().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);
}
private boolean isValidPassword(String password) {
if (password == null || password.length() < 8) {
return false;
@ -80,4 +146,21 @@ public class AuthService {
return hasUppercase && hasLowercase && hasDigit && hasSpecial;
}
private boolean isValidEmail(String email) {
if (email == null) {
return false;
}
String normalizedEmail = email.trim();
if (normalizedEmail.isEmpty() || normalizedEmail.contains(" ")) {
return false;
}
int atIndex = normalizedEmail.indexOf('@');
int lastAtIndex = normalizedEmail.lastIndexOf('@');
int dotIndex = normalizedEmail.lastIndexOf('.');
return atIndex > 0
&& atIndex == lastAtIndex
&& dotIndex > atIndex + 1
&& dotIndex < normalizedEmail.length() - 1;
}
}

View File

@ -48,12 +48,8 @@ public class UserImportComponent {
ObjectMapperHolder.mapper.getTypeFactory().constructCollectionType(List.class, LoginEntity.class));
for (LoginEntity user : users) {
authRepository.findById(user.getUsername()).ifPresentOrElse(
existingUser -> {
logger.debug("User {} already present in the database, updating password",
user.getUsername());
existingUser.setPassword(PasswordHasher.hashPassword(user.getPassword()));
authRepository.save(existingUser);
},
existingUser -> logger.debug("User {} already present in the database, skipping import",
user.getUsername()),
() -> {
logger.debug("User {} not found, saving new user", user.getUsername());
user.setPassword(PasswordHasher.hashPassword(user.getPassword()));

View File

@ -4,21 +4,35 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import it.cnr.isti.workflow.manager.auth.config.JwtUtil;
import it.cnr.isti.workflow.manager.auth.model.AdminChangePasswordResult;
import it.cnr.isti.workflow.manager.auth.model.AdminChangeUserPasswordRequest;
import it.cnr.isti.workflow.manager.auth.model.AdminChangeUserRoleRequest;
import it.cnr.isti.workflow.manager.auth.model.AdminCreateUserRequest;
import it.cnr.isti.workflow.manager.auth.model.AuthRequest;
import it.cnr.isti.workflow.manager.auth.model.ChangePasswordResult;
import it.cnr.isti.workflow.manager.auth.model.ChangeUserRoleResult;
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 java.util.Collections;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PutMapping;
import org.eclipse.microprofile.openapi.annotations.Operation;
import java.util.LinkedHashMap;
import java.util.Map;
@RestController
@RequestMapping("/auth")
public class AuthController {
@ -43,7 +57,13 @@ public class AuthController {
return ResponseEntity.status(401).body("Invalid password");
}
String jwt = jwtUtil.generateToken(request.getUsername());
return ResponseEntity.ok(Collections.singletonMap("token", jwt));
var user = authService.getUser(request.getUsername());
Map<String, Object> response = new LinkedHashMap<>();
response.put("token", jwt);
response.put("username", request.getUsername());
response.put("role", user == null ? "USER" : user.effectiveRole().name());
response.put("email", user == null ? null : user.getEmail());
return ResponseEntity.ok(response);
} catch (UsernameNotFoundException e) {
return ResponseEntity.status(404).body("User "+request.getUsername()+" not found");
}
@ -52,18 +72,21 @@ public class AuthController {
@PostMapping("/register")
@Operation(summary = "Register user", description = "Registers a new user account with username and password.")
public ResponseEntity<?> register(@RequestBody AuthRequest request) {
logger.info("Register attempt for user {} with pwd {}", request.getUsername(), request.getPassword());
if (request.getUsername() == null || request.getPassword() == null) {
return ResponseEntity.badRequest().body("Username and password are required");
logger.info("Register attempt for user {}", request.getUsername());
if (request.getUsername() == null || request.getPassword() == null || request.getEmail() == null) {
return ResponseEntity.badRequest().body("Username, password and email are required");
}
if (request.getUsername().length() < 3 || request.getPassword().length() < 6) {
if (request.getUsername().length() < 3) {
return ResponseEntity.badRequest()
.body("Username must be at least 3 characters and password at least 6 characters");
.body("Username must be at least 3 characters");
}
if (authService.registerUser(request.getUsername(), request.getPassword())) {
return ResponseEntity.ok().build();
}
return ResponseEntity.badRequest().body("the user already exists");
CreateUserResult result = authService.registerUser(request.getUsername(), request.getPassword(), request.getEmail());
return switch (result) {
case SUCCESS -> ResponseEntity.ok().build();
case USER_ALREADY_EXISTS -> ResponseEntity.badRequest().body("the user already exists");
case INVALID_EMAIL -> ResponseEntity.badRequest().body("INVALID_EMAIL");
case INVALID_PASSWORD -> ResponseEntity.badRequest().body("INVALID_PASSWORD");
};
}
@PostMapping("/change-password")
@ -77,7 +100,7 @@ public class AuthController {
.body("Username, oldPassword/currentPassword and newPassword are required");
}
AuthService.ChangePasswordResult result = authService.changePassword(
ChangePasswordResult result = authService.changePassword(
request.getUsername(),
request.getOldPassword(),
request.getNewPassword());
@ -93,6 +116,77 @@ public class AuthController {
};
}
@GetMapping("/admin/users")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "List users", description = "Returns the list of users with their role.")
public ResponseEntity<?> listUsers() {
return ResponseEntity.ok(authService.listUsers());
}
@PostMapping("/admin/users")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Create user", description = "Creates a new user with USER or ADMIN role.")
public ResponseEntity<?> createUser(@RequestBody AdminCreateUserRequest request) {
if (request == null || isBlank(request.username()) || isBlank(request.password()) || isBlank(request.email())) {
return ResponseEntity.badRequest().body("username, password and email are required");
}
CreateUserResult result = authService.createUser(request.username(), request.password(), request.email(), request.role());
return switch (result) {
case SUCCESS -> ResponseEntity.ok().build();
case USER_ALREADY_EXISTS -> ResponseEntity.badRequest().body("the user already exists");
case INVALID_EMAIL -> ResponseEntity.badRequest().body("INVALID_EMAIL");
case INVALID_PASSWORD -> ResponseEntity.badRequest().body("INVALID_PASSWORD");
};
}
@PutMapping("/admin/users/{username}/password")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Change user password", description = "Admin operation to reset another user's password.")
public ResponseEntity<?> adminChangePassword(@PathVariable String username,
@RequestBody AdminChangeUserPasswordRequest request) {
if (request == null || isBlank(username) || isBlank(request.newPassword())) {
return ResponseEntity.badRequest().body("username and newPassword are required");
}
AdminChangePasswordResult result = authService.adminChangePassword(username, request.newPassword());
return switch (result) {
case SUCCESS -> ResponseEntity.ok().build();
case USER_NOT_FOUND -> ResponseEntity.status(HttpStatus.NOT_FOUND).body("User " + username + " not found");
case INVALID_PASSWORD -> ResponseEntity.badRequest().body("INVALID_PASSWORD");
};
}
@PutMapping("/admin/users/{username}/role")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Change user role", description = "Admin operation to change a user's role.")
public ResponseEntity<?> adminChangeUserRole(@PathVariable String username,
@RequestBody AdminChangeUserRoleRequest request) {
if (request == null || isBlank(username) || request.role() == null) {
return ResponseEntity.badRequest().body("username and role are required");
}
ChangeUserRoleResult result = authService.changeUserRole(username, request.role());
return switch (result) {
case SUCCESS -> ResponseEntity.ok().build();
case USER_NOT_FOUND -> ResponseEntity.status(HttpStatus.NOT_FOUND).body("User " + username + " not found");
case INVALID_ROLE -> ResponseEntity.badRequest().body("INVALID_ROLE");
case LAST_ADMIN -> ResponseEntity.status(HttpStatus.CONFLICT).body("LAST_ADMIN");
};
}
@DeleteMapping("/admin/users/{username}")
@PreAuthorize("hasRole('ADMIN')")
@Operation(summary = "Delete user", description = "Removes a user.")
public ResponseEntity<?> deleteUser(@PathVariable String username) {
if (isBlank(username)) {
return ResponseEntity.badRequest().body("username is required");
}
DeleteUserResult result = authService.deleteUser(username);
return switch (result) {
case SUCCESS -> ResponseEntity.ok().build();
case USER_NOT_FOUND -> ResponseEntity.status(HttpStatus.NOT_FOUND).body("User " + username + " not found");
case LAST_ADMIN -> ResponseEntity.status(HttpStatus.CONFLICT).body("LAST_ADMIN");
};
}
private boolean isBlank(String value) {
return value == null || value.isBlank();
}

View File

@ -82,7 +82,8 @@ public class ExecutionsService {
ExecutionObject toReturn = executions.get(id);
if (toReturn == null) {
ExecutionEntity entity = executionRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("Execution with id " + id + " not found"));
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Execution with id " + id + " not found"));
toReturn = rebuildExecution(entity);
executions.put(id, toReturn);
}
@ -100,8 +101,9 @@ public class ExecutionsService {
if (execution == null && executionRepository.existsById(id)) {
execution = getExecution(id);
}
if (execution == null)
throw new IllegalArgumentException("Execution with id " + id + " not found");
if (execution == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Execution with id " + id + " not found");
}
if (execution.getContext().getStatus() == ExecutionStatus.RUNNING)
throw new IllegalStateException("Execution with id " + id + " is still running");
executions.remove(id);

View File

@ -1,6 +1,7 @@
[
{
"username": "testuser",
"password": "testpassword"
"password": "testpassword",
"email": "testuser@example.com"
}
]

View File

@ -1,26 +1,54 @@
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.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.HttpStatus;
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.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import it.cnr.isti.workflow.manager.auth.config.JwtUtil;
import it.cnr.isti.workflow.manager.auth.model.AdminChangeUserPasswordRequest;
import it.cnr.isti.workflow.manager.auth.model.AdminChangeUserRoleRequest;
import it.cnr.isti.workflow.manager.auth.model.AdminCreateUserRequest;
import it.cnr.isti.workflow.manager.auth.model.AuthRequest;
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;
@SpringBootTest
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:test.properties")
public class AuthControllerTest {
@Autowired
private AuthController authController;
@Autowired
private AuthRepository authRepository;
@Autowired
private MockMvc mockMvc;
@Autowired
private JwtUtil jwtUtil;
@Test
public void testLogin() {
ResponseEntity<?> response = authController.login(new AuthRequest("testuser", "testpassword"));
ResponseEntity<?> response = authController.login(new AuthRequest("testuser", "testpassword", "testuser@example.com"));
assert response.getStatusCode().is2xxSuccessful();
assert response.getBody() instanceof java.util.Map<?, ?>;
assert "USER".equals(((java.util.Map<?, ?>) response.getBody()).get("role"));
assert ((java.util.Map<?, ?>) response.getBody()).containsKey("email");
}
@Test
@ -33,7 +61,7 @@ public class AuthControllerTest {
ResponseEntity<?> response = authController.changePassword(request);
assert response.getStatusCode().is2xxSuccessful();
ResponseEntity<?> loginResponse = authController.login(new AuthRequest("testuser", "Newpassword1!"));
ResponseEntity<?> loginResponse = authController.login(new AuthRequest("testuser", "Newpassword1!", "testuser@example.com"));
assert loginResponse.getStatusCode().is2xxSuccessful();
}
@ -50,7 +78,7 @@ public class AuthControllerTest {
@Test
public void testChangePasswordReturnsInvalidNewPasswordForWeakPassword() {
authController.register(new AuthRequest("weakpwduser", "Startpass1!"));
authController.register(new AuthRequest("weakpwduser", "Startpass1!", "weakpwduser@example.com"));
ChangePasswordRequest request = new ChangePasswordRequest();
request.setUsername("weakpwduser");
@ -61,4 +89,125 @@ public class AuthControllerTest {
assert response.getStatusCode() == HttpStatus.BAD_REQUEST;
assert "INVALID_NEW_PASSWORD".equals(response.getBody());
}
@Test
public void adminCanCreateListChangePasswordAndDeleteUsers() {
LoginEntity admin = new LoginEntity("adminuser",
it.cnr.isti.workflow.manager.auth.services.PasswordHasher.hashPassword("Adminpass1!"),
"adminuser@example.com",
UserRole.ADMIN);
authRepository.save(admin);
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
admin,
null,
java.util.List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))));
try {
ResponseEntity<?> createResponse = authController.createUser(
new AdminCreateUserRequest("manageduser", "Managedpass1!", "manageduser@example.com", 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"));
ResponseEntity<?> passwordResponse = authController.adminChangePassword(
"manageduser",
new AdminChangeUserPasswordRequest("Changedpass1!"));
assert passwordResponse.getStatusCode().is2xxSuccessful();
ResponseEntity<?> roleResponse = authController.adminChangeUserRole(
"manageduser",
new AdminChangeUserRoleRequest(UserRole.ADMIN));
assert roleResponse.getStatusCode().is2xxSuccessful();
assert authRepository.findById("manageduser").orElseThrow().effectiveRole() == UserRole.ADMIN;
ResponseEntity<?> loginResponse = authController.login(new AuthRequest("manageduser", "Changedpass1!", "manageduser@example.com"));
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"));
ResponseEntity<?> deleteResponse = authController.deleteUser("manageduser");
assert deleteResponse.getStatusCode().is2xxSuccessful();
} finally {
SecurityContextHolder.clearContext();
}
}
@Test
public void cannotDowngradeLastAdmin() {
authRepository.findAll().stream()
.filter(user -> user.effectiveRole() == UserRole.ADMIN)
.map(LoginEntity::getUsername)
.forEach(authRepository::deleteById);
LoginEntity admin = new LoginEntity("soloadmin",
it.cnr.isti.workflow.manager.auth.services.PasswordHasher.hashPassword("Adminpass1!"),
"soloadmin@example.com",
UserRole.ADMIN);
authRepository.save(admin);
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
admin,
null,
java.util.List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))));
try {
ResponseEntity<?> roleResponse = authController.adminChangeUserRole(
"soloadmin",
new AdminChangeUserRoleRequest(UserRole.USER));
assert roleResponse.getStatusCode() == HttpStatus.CONFLICT;
assert "LAST_ADMIN".equals(roleResponse.getBody());
} finally {
SecurityContextHolder.clearContext();
}
}
@Test
public void cannotDeleteLastAdmin() {
authRepository.findAll().stream()
.filter(user -> user.effectiveRole() == UserRole.ADMIN)
.map(LoginEntity::getUsername)
.forEach(authRepository::deleteById);
LoginEntity admin = new LoginEntity("onlyadmin",
it.cnr.isti.workflow.manager.auth.services.PasswordHasher.hashPassword("Adminpass1!"),
"onlyadmin@example.com",
UserRole.ADMIN);
authRepository.save(admin);
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(
admin,
null,
java.util.List.of(new SimpleGrantedAuthority("ROLE_ADMIN"))));
try {
ResponseEntity<?> deleteResponse = authController.deleteUser("onlyadmin");
assert deleteResponse.getStatusCode() == HttpStatus.CONFLICT;
assert "LAST_ADMIN".equals(deleteResponse.getBody());
} finally {
SecurityContextHolder.clearContext();
}
}
@Test
public void registerRejectsInvalidEmail() {
ResponseEntity<?> response = authController.register(new AuthRequest("mailuser", "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",
it.cnr.isti.workflow.manager.auth.services.PasswordHasher.hashPassword("Adminpass1!"),
"jwtadmin@example.com",
UserRole.ADMIN);
authRepository.save(admin);
String token = jwtUtil.generateToken(admin.getUsername());
mockMvc.perform(get("/auth/admin/users")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk());
}
}

View File

@ -209,6 +209,14 @@ public class ExecutionControllerTest {
org.junit.jupiter.api.Assertions.assertTrue(events.stream().anyMatch(event -> event.getType() == ExecutionEventType.LLM_REQUEST));
}
@Test
public void getMissingExecutionReturnsNotFound() {
ResponseStatusException exception = org.junit.jupiter.api.Assertions.assertThrows(
ResponseStatusException.class,
() -> executionsController.get("missing-execution-id"));
org.junit.jupiter.api.Assertions.assertEquals(HttpStatus.NOT_FOUND, exception.getStatusCode());
}
@Test
public void executionPayloadDoesNotSerializeEvents() throws JsonProcessingException {
LLMDescriptor llmDescriptor = LLMDescriptor.builder()

View File

@ -327,6 +327,15 @@ public class FlowControllerTest {
assertEquals(404, response.getStatusCode().value());
}
@Test
public void getMissingFlowReturnsNotFound() {
ResponseEntity<FlowView> response = flowController.getFlow(
"missing-id",
new LoginEntity("testuser", "testpassword"));
assertEquals(404, response.getStatusCode().value());
}
@Test
public void deleteFlow() {
LLMDescriptor llmDescriptor = LLMDescriptor.builder()