users administration api added
This commit is contained in:
parent
99472ffec4
commit
7d7b131f55
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
package it.cnr.isti.workflow.manager.auth.model;
|
||||
|
||||
public enum AdminChangePasswordResult {
|
||||
SUCCESS,
|
||||
USER_NOT_FOUND,
|
||||
INVALID_PASSWORD
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package it.cnr.isti.workflow.manager.auth.model;
|
||||
|
||||
public record AdminChangeUserPasswordRequest(
|
||||
String newPassword) {
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package it.cnr.isti.workflow.manager.auth.model;
|
||||
|
||||
public record AdminChangeUserRoleRequest(
|
||||
UserRole role) {
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package it.cnr.isti.workflow.manager.auth.model;
|
||||
|
||||
public record AdminCreateUserRequest(
|
||||
String username,
|
||||
String password,
|
||||
String email,
|
||||
UserRole role) {
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package it.cnr.isti.workflow.manager.auth.model;
|
||||
|
||||
public enum ChangeUserRoleResult {
|
||||
SUCCESS,
|
||||
USER_NOT_FOUND,
|
||||
INVALID_ROLE,
|
||||
LAST_ADMIN
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package it.cnr.isti.workflow.manager.auth.model;
|
||||
|
||||
public enum CreateUserResult {
|
||||
SUCCESS,
|
||||
USER_ALREADY_EXISTS,
|
||||
INVALID_EMAIL,
|
||||
INVALID_PASSWORD
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package it.cnr.isti.workflow.manager.auth.model;
|
||||
|
||||
public enum DeleteUserResult {
|
||||
SUCCESS,
|
||||
USER_NOT_FOUND,
|
||||
LAST_ADMIN
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package it.cnr.isti.workflow.manager.auth.model;
|
||||
|
||||
public enum UserRole {
|
||||
USER,
|
||||
ADMIN
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package it.cnr.isti.workflow.manager.auth.model;
|
||||
|
||||
public record UserView(
|
||||
String username,
|
||||
String email,
|
||||
UserRole role) {
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
[
|
||||
{
|
||||
"username": "testuser",
|
||||
"password": "testpassword"
|
||||
"password": "testpassword",
|
||||
"email": "testuser@example.com"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Reference in New Issue