用户/账号管理,系统日志
This commit is contained in:
@@ -0,0 +1,263 @@
|
||||
package com.mzh.library.service;
|
||||
|
||||
import com.mzh.library.dao.SystemLogDao;
|
||||
import com.mzh.library.dao.UserAccountDao;
|
||||
import com.mzh.library.entity.AuthenticatedUser;
|
||||
import com.mzh.library.entity.Permission;
|
||||
import com.mzh.library.entity.Role;
|
||||
import com.mzh.library.entity.SystemLog;
|
||||
import com.mzh.library.entity.SystemLogSearchCriteria;
|
||||
import com.mzh.library.entity.User;
|
||||
import com.mzh.library.entity.UserSearchCriteria;
|
||||
import com.mzh.library.exception.DaoException;
|
||||
import com.mzh.library.service.impl.UserAccountServiceImpl;
|
||||
import com.mzh.library.util.PasswordHasher;
|
||||
|
||||
import java.sql.Connection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class UserAccountServiceCheck {
|
||||
private static final String DENIED_MESSAGE = "You do not have permission to manage users.";
|
||||
private static final String UNAVAILABLE_MESSAGE =
|
||||
"User management service is temporarily unavailable. Please try again later.";
|
||||
|
||||
private UserAccountServiceCheck() {
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Logger.getLogger(UserAccountServiceImpl.class.getName()).setLevel(Level.OFF);
|
||||
|
||||
InMemoryUserAccountDao userDao = new InMemoryUserAccountDao();
|
||||
InMemorySystemLogDao logDao = new InMemorySystemLogDao();
|
||||
User adminAccount = user(1L, "admin", "System Administrator", Role.ADMINISTRATOR, true, "admin-password");
|
||||
User staffAccount = user(2L, "staff", "Library Staff", Role.LIBRARIAN, true, "staff-password");
|
||||
userDao.put(adminAccount);
|
||||
userDao.put(staffAccount);
|
||||
|
||||
UserAccountService service = new UserAccountServiceImpl(userDao, logDao, new PermissionPolicy(),
|
||||
new UserAccountServiceImpl.DirectTransactionExecutor());
|
||||
AuthenticatedUser admin = actor(1L, Role.ADMINISTRATOR);
|
||||
AuthenticatedUser reader = actor(3L, Role.READER);
|
||||
|
||||
requireMessage(service.searchUsers(reader, new UserSearchCriteria()), DENIED_MESSAGE);
|
||||
|
||||
User duplicate = user(0L, " admin ", "Duplicate Admin", Role.ADMINISTRATOR, true, "unused");
|
||||
ServiceResult<Long> duplicateResult = service.createUser(admin, duplicate, "new-password", "127.0.0.1");
|
||||
require(!duplicateResult.isSuccessful(), "duplicate username should be rejected");
|
||||
require(duplicateResult.getErrors().containsKey("username"), "duplicate username should target username");
|
||||
|
||||
User invalid = new User();
|
||||
ServiceResult<Long> invalidResult = service.createUser(admin, invalid, "", "127.0.0.1");
|
||||
require(!invalidResult.isSuccessful(), "invalid user should be rejected");
|
||||
require(invalidResult.getErrors().containsKey("username"), "missing username should be reported");
|
||||
require(invalidResult.getErrors().containsKey("displayName"), "missing display name should be reported");
|
||||
require(invalidResult.getErrors().containsKey("password"), "missing password should be reported");
|
||||
|
||||
User newReader = new User();
|
||||
newReader.setUsername(" new.reader ");
|
||||
newReader.setDisplayName("New Reader");
|
||||
newReader.setRole(Role.READER);
|
||||
newReader.setActive(true);
|
||||
ServiceResult<Long> created = service.createUser(admin, newReader, "reader-password", "127.0.0.1");
|
||||
require(created.isSuccessful(), "administrator should create user accounts");
|
||||
User storedReader = userDao.findById(created.getData()).orElseThrow(AssertionError::new);
|
||||
require("new.reader".equals(storedReader.getUsername()), "username should be trimmed like login");
|
||||
require(!"reader-password".equals(storedReader.getPasswordHash()), "password should not be stored in plain text");
|
||||
require(PasswordHasher.verify("reader-password", storedReader.getPasswordHash()), "stored password should verify");
|
||||
require(logDao.logs.size() == 1, "user creation should write one system log");
|
||||
require("user.create".equals(logDao.logs.get(0).getOperationType()), "creation log should use user.create");
|
||||
|
||||
User selfRoleChange = user(1L, "admin", "System Administrator", Role.LIBRARIAN, true, "ignored");
|
||||
ServiceResult<Void> selfRoleResult = service.updateUser(admin, selfRoleChange, "", "127.0.0.1");
|
||||
require(!selfRoleResult.isSuccessful(), "current administrator role change should be blocked");
|
||||
require(selfRoleResult.getErrors().containsKey("role"), "self role change should target role");
|
||||
|
||||
User selfDeactivate = user(1L, "admin", "System Administrator", Role.ADMINISTRATOR, false, "ignored");
|
||||
ServiceResult<Void> selfDeactivateResult = service.updateUser(admin, selfDeactivate, "", "127.0.0.1");
|
||||
require(!selfDeactivateResult.isSuccessful(), "current administrator deactivation should be blocked");
|
||||
require(selfDeactivateResult.getErrors().containsKey("active"), "self deactivation should target active state");
|
||||
|
||||
String originalStaffHash = staffAccount.getPasswordHash();
|
||||
User updatedStaff = user(2L, "staff", "Lead Librarian", Role.LIBRARIAN, true, "ignored");
|
||||
ServiceResult<Void> updated = service.updateUser(admin, updatedStaff, "", "127.0.0.1");
|
||||
require(updated.isSuccessful(), "administrator should update user accounts");
|
||||
require(originalStaffHash.equals(userDao.findById(2L).orElseThrow(AssertionError::new).getPasswordHash()),
|
||||
"blank update password should preserve existing hash");
|
||||
|
||||
ServiceResult<Void> reset = service.updateUser(admin, updatedStaff, "replacement-password", "127.0.0.1");
|
||||
require(reset.isSuccessful(), "administrator should reset passwords");
|
||||
require(PasswordHasher.verify("replacement-password",
|
||||
userDao.findById(2L).orElseThrow(AssertionError::new).getPasswordHash()),
|
||||
"replacement password should be hashed");
|
||||
|
||||
ServiceResult<Void> deactivated = service.deactivateUser(admin, 2L, "127.0.0.1");
|
||||
require(deactivated.isSuccessful(), "administrator should deactivate other accounts");
|
||||
require(!userDao.findById(2L).orElseThrow(AssertionError::new).isActive(),
|
||||
"deactivate action should mark account inactive");
|
||||
require(logDao.logs.stream().anyMatch(log -> "user.deactivate".equals(log.getOperationType())),
|
||||
"deactivate should write a system log");
|
||||
|
||||
UserAccountService failingService = new UserAccountServiceImpl(new FailingUserAccountDao(), logDao,
|
||||
new PermissionPolicy(), new UserAccountServiceImpl.DirectTransactionExecutor());
|
||||
requireMessage(failingService.searchUsers(admin, new UserSearchCriteria()), UNAVAILABLE_MESSAGE);
|
||||
}
|
||||
|
||||
private static User user(long id, String username, String displayName, Role role, boolean active, String password) {
|
||||
User user = new User();
|
||||
user.setId(id);
|
||||
user.setUsername(username);
|
||||
user.setDisplayName(displayName);
|
||||
user.setRole(role);
|
||||
user.setActive(active);
|
||||
user.setPasswordHash(PasswordHasher.hash(password));
|
||||
return user;
|
||||
}
|
||||
|
||||
private static AuthenticatedUser actor(long id, Role role) {
|
||||
return new AuthenticatedUser(id, role.getCode(), role.getDisplayName(), role,
|
||||
role == Role.ADMINISTRATOR
|
||||
? EnumSet.allOf(Permission.class)
|
||||
: EnumSet.of(Permission.VIEW_CATALOG, Permission.BORROW_BOOKS));
|
||||
}
|
||||
|
||||
private static void requireMessage(ServiceResult<?> result, String message) {
|
||||
require(!result.isSuccessful(), "result should be a failure");
|
||||
require(message.equals(result.getMessage()), "expected message: " + message);
|
||||
}
|
||||
|
||||
private static void require(boolean condition, String message) {
|
||||
if (!condition) {
|
||||
throw new AssertionError(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemoryUserAccountDao implements UserAccountDao {
|
||||
private final Map<Long, User> users = new LinkedHashMap<>();
|
||||
private long nextId = 10L;
|
||||
|
||||
private void put(User user) {
|
||||
users.put(user.getId(), copy(user));
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> search(UserSearchCriteria criteria) {
|
||||
return users.values().stream()
|
||||
.filter(user -> criteria.getRoleCode().isEmpty()
|
||||
|| user.getRole().getCode().equals(criteria.getRoleCode()))
|
||||
.filter(user -> criteria.getActiveValue() == null
|
||||
|| user.isActive() == criteria.getActiveValue())
|
||||
.map(this::copy)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findById(long id) {
|
||||
return Optional.ofNullable(users.get(id)).map(this::copy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findByUsername(String username) {
|
||||
return users.values().stream()
|
||||
.filter(user -> user.getUsername().equals(username))
|
||||
.findFirst()
|
||||
.map(this::copy);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long create(Connection connection, User user) {
|
||||
long id = nextId++;
|
||||
User stored = copy(user);
|
||||
stored.setId(id);
|
||||
users.put(id, stored);
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(Connection connection, User user, boolean updatePassword) {
|
||||
User existing = users.get(user.getId());
|
||||
if (existing == null) {
|
||||
return false;
|
||||
}
|
||||
existing.setDisplayName(user.getDisplayName());
|
||||
existing.setRole(user.getRole());
|
||||
existing.setActive(user.isActive());
|
||||
if (updatePassword) {
|
||||
existing.setPasswordHash(user.getPasswordHash());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private User copy(User source) {
|
||||
User copy = new User();
|
||||
copy.setId(source.getId());
|
||||
copy.setUsername(source.getUsername());
|
||||
copy.setDisplayName(source.getDisplayName());
|
||||
copy.setRole(source.getRole());
|
||||
copy.setActive(source.isActive());
|
||||
copy.setPasswordHash(source.getPasswordHash());
|
||||
copy.setCreatedAt(source.getCreatedAt());
|
||||
copy.setUpdatedAt(source.getUpdatedAt());
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemorySystemLogDao implements SystemLogDao {
|
||||
private final List<SystemLog> logs = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public List<SystemLog> search(SystemLogSearchCriteria criteria) {
|
||||
return new ArrayList<>(logs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> findOperationTypes() {
|
||||
return logs.stream()
|
||||
.map(SystemLog::getOperationType)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public long create(Connection connection, SystemLog log) {
|
||||
log.setId(logs.size() + 1L);
|
||||
logs.add(log);
|
||||
return log.getId();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class FailingUserAccountDao implements UserAccountDao {
|
||||
@Override
|
||||
public List<User> search(UserSearchCriteria criteria) {
|
||||
throw new DaoException("Simulated user search failure", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findById(long id) {
|
||||
throw new DaoException("Simulated user lookup failure", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<User> findByUsername(String username) {
|
||||
throw new DaoException("Simulated username lookup failure", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long create(Connection connection, User user) {
|
||||
throw new DaoException("Simulated user create failure", null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean update(Connection connection, User user, boolean updatePassword) {
|
||||
throw new DaoException("Simulated user update failure", null);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user