Harden admin guards
This commit is contained in:
@@ -3,6 +3,7 @@ package com.petshop.backend.controller;
|
||||
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
||||
import com.petshop.backend.dto.user.UserRequest;
|
||||
import com.petshop.backend.dto.user.UserResponse;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.service.UserService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -32,30 +33,30 @@ public class CustomerController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<UserResponse> getCustomerById(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(userService.getUserById(id));
|
||||
return ResponseEntity.ok(userService.getUserById(id, User.Role.CUSTOMER));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<UserResponse> createCustomer(@Valid @RequestBody UserRequest request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(userService.createUser(request));
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(userService.createUser(request, User.Role.CUSTOMER));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<UserResponse> updateCustomer(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody UserRequest request) {
|
||||
return ResponseEntity.ok(userService.updateUser(id, request));
|
||||
return ResponseEntity.ok(userService.updateUser(id, request, User.Role.CUSTOMER));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteCustomer(@PathVariable Long id) {
|
||||
userService.deleteUser(id);
|
||||
userService.deleteUser(id, User.Role.CUSTOMER);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/bulk-delete")
|
||||
public ResponseEntity<Void> bulkDeleteCustomers(@Valid @RequestBody BulkDeleteRequest request) {
|
||||
userService.bulkDeleteUsers(request);
|
||||
userService.bulkDeleteUsers(request, User.Role.CUSTOMER);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.petshop.backend.controller;
|
||||
|
||||
import com.petshop.backend.dto.user.UserRequest;
|
||||
import com.petshop.backend.dto.user.UserResponse;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.service.UserService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -31,24 +32,24 @@ public class EmployeeController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<UserResponse> getEmployeeById(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(userService.getUserById(id));
|
||||
return ResponseEntity.ok(userService.getUserById(id, User.Role.STAFF));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<UserResponse> createEmployee(@Valid @RequestBody UserRequest request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(userService.createUser(request));
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(userService.createUser(request, User.Role.STAFF));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<UserResponse> updateEmployee(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody UserRequest request) {
|
||||
return ResponseEntity.ok(userService.updateUser(id, request));
|
||||
return ResponseEntity.ok(userService.updateUser(id, request, User.Role.STAFF));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteEmployee(@PathVariable Long id) {
|
||||
userService.deleteUser(id);
|
||||
userService.deleteUser(id, User.Role.STAFF);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,21 +3,27 @@ package com.petshop.backend.service;
|
||||
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
||||
import com.petshop.backend.dto.user.UserRequest;
|
||||
import com.petshop.backend.dto.user.UserResponse;
|
||||
import com.petshop.backend.repository.ActivityLogRepository;
|
||||
import com.petshop.backend.entity.StoreLocation;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.ActivityLogRepository;
|
||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||
import com.petshop.backend.repository.StoreRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import com.petshop.backend.util.AuthenticationHelper;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||
import static org.springframework.http.HttpStatus.CONFLICT;
|
||||
@@ -54,13 +60,26 @@ public class UserService {
|
||||
}
|
||||
|
||||
public UserResponse getUserById(Long id) {
|
||||
return getUserById(id, null);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public UserResponse getUserById(Long id, User.Role requiredRole) {
|
||||
User user = userRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
|
||||
requireRoleOrNotFound(user, requiredRole, id);
|
||||
return mapToResponse(user);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserResponse createUser(UserRequest request) {
|
||||
return createUser(request, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserResponse createUser(UserRequest request, User.Role requiredRole) {
|
||||
requireRequestedRole(request.getRole(), requiredRole);
|
||||
|
||||
User user = new User();
|
||||
user.setUsername(trimToNull(request.getUsername()));
|
||||
if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
|
||||
@@ -88,9 +107,18 @@ public class UserService {
|
||||
|
||||
@Transactional
|
||||
public UserResponse updateUser(Long id, UserRequest request) {
|
||||
return updateUser(id, request, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UserResponse updateUser(Long id, UserRequest request, User.Role requiredRole) {
|
||||
User user = userRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
|
||||
|
||||
requireRoleOrNotFound(user, requiredRole, id);
|
||||
requireRequestedRole(request.getRole(), requiredRole);
|
||||
requireAdminMutationAllowed(user, request.getRole());
|
||||
|
||||
boolean invalidateToken =
|
||||
!Objects.equals(user.getUsername(), request.getUsername())
|
||||
|| user.getRole() != request.getRole()
|
||||
@@ -127,9 +155,17 @@ public class UserService {
|
||||
|
||||
@Transactional
|
||||
public void deleteUser(Long id) {
|
||||
if (!userRepository.existsById(id)) {
|
||||
throw new ResourceNotFoundException("User not found with id: " + id);
|
||||
}
|
||||
deleteUser(id, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteUser(Long id, User.Role requiredRole) {
|
||||
User user = userRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
|
||||
|
||||
requireRoleOrNotFound(user, requiredRole, id);
|
||||
requireAdminDeletionAllowed(user);
|
||||
|
||||
if (activityLogRepository.existsByUser_Id(id)) {
|
||||
throw new ResponseStatusException(CONFLICT, "User cannot be deleted because activity logs exist");
|
||||
}
|
||||
@@ -138,7 +174,27 @@ public class UserService {
|
||||
|
||||
@Transactional
|
||||
public void bulkDeleteUsers(BulkDeleteRequest request) {
|
||||
if (request.getIds() != null && request.getIds().stream().anyMatch(activityLogRepository::existsByUser_Id)) {
|
||||
bulkDeleteUsers(request, null);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void bulkDeleteUsers(BulkDeleteRequest request, User.Role requiredRole) {
|
||||
if (request.getIds() == null || request.getIds().isEmpty()) {
|
||||
throw new ResponseStatusException(BAD_REQUEST, "IDs list cannot be empty");
|
||||
}
|
||||
|
||||
Set<Long> requestedIds = new HashSet<>(request.getIds());
|
||||
ArrayList<User> usersToDelete = new ArrayList<>();
|
||||
userRepository.findAllById(requestedIds).forEach(usersToDelete::add);
|
||||
|
||||
if (usersToDelete.size() != requestedIds.size()) {
|
||||
throw new ResourceNotFoundException("One or more users not found for bulk delete");
|
||||
}
|
||||
|
||||
requireRoleOrNotFound(usersToDelete, requiredRole);
|
||||
requireAdminDeletionAllowed(usersToDelete);
|
||||
|
||||
if (request.getIds().stream().anyMatch(activityLogRepository::existsByUser_Id)) {
|
||||
throw new ResponseStatusException(CONFLICT, "One or more users cannot be deleted because activity logs exist");
|
||||
}
|
||||
userRepository.deleteAllById(request.getIds());
|
||||
@@ -169,6 +225,51 @@ public class UserService {
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId));
|
||||
}
|
||||
|
||||
private void requireRoleOrNotFound(User user, User.Role requiredRole, Long id) {
|
||||
if (requiredRole != null && user.getRole() != requiredRole) {
|
||||
throw new ResourceNotFoundException("User not found with id: " + id);
|
||||
}
|
||||
}
|
||||
|
||||
private void requireRoleOrNotFound(Collection<User> users, User.Role requiredRole) {
|
||||
if (requiredRole == null) {
|
||||
return;
|
||||
}
|
||||
for (User user : users) {
|
||||
if (user.getRole() != requiredRole) {
|
||||
throw new ResourceNotFoundException("User not found with id: " + user.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void requireRequestedRole(User.Role requestedRole, User.Role requiredRole) {
|
||||
if (requiredRole != null && requestedRole != requiredRole) {
|
||||
throw new AccessDeniedException("Target user must have role " + requiredRole.name());
|
||||
}
|
||||
}
|
||||
|
||||
private void requireAdminMutationAllowed(User target, User.Role requestedRole) {
|
||||
User actor = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||
if ((target.getRole() == User.Role.ADMIN && !actor.getId().equals(target.getId()))
|
||||
|| (requestedRole == User.Role.ADMIN && !actor.getId().equals(target.getId()))) {
|
||||
throw new AccessDeniedException("Admins cannot modify other admin accounts");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireAdminDeletionAllowed(User target) {
|
||||
if (target.getRole() == User.Role.ADMIN) {
|
||||
throw new AccessDeniedException("Admins cannot delete admin accounts");
|
||||
}
|
||||
}
|
||||
|
||||
private void requireAdminDeletionAllowed(Collection<User> targets) {
|
||||
for (User target : targets) {
|
||||
if (target.getRole() == User.Role.ADMIN) {
|
||||
throw new AccessDeniedException("Admins cannot delete admin accounts");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void validateUniquePhone(String phone, Long currentUserId) {
|
||||
if (phone == null || phone.isBlank()) {
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user