[Developer] #424 add user and permission management

Refs #424
This commit is contained in:
devmrko
2026-06-23 11:28:59 +09:00
parent c55e7add27
commit 16718f0277
23 changed files with 708 additions and 3 deletions

View File

@@ -0,0 +1,12 @@
package com.cloudhandson.vpdbackoffice.domain.permission;
public record PermissionView(
long permissionId,
long roleId,
String roleName,
long objectId,
String objectName,
String action,
String rules
) {
}

View File

@@ -0,0 +1,12 @@
package com.cloudhandson.vpdbackoffice.domain.protectedobject;
import jakarta.validation.constraints.NotBlank;
public record ProtectedObjectCreateCommand(
@NotBlank String owner,
@NotBlank String objectName,
@NotBlank String ordsPath,
String columns,
String sensitiveColumns
) {
}

View File

@@ -5,6 +5,7 @@ public record AppUser(
String username,
String empNo,
String deptCode,
String canReadContents,
String activeYn
) {

View File

@@ -0,0 +1,11 @@
package com.cloudhandson.vpdbackoffice.domain.user;
import jakarta.validation.constraints.NotBlank;
public record UserCreateCommand(
@NotBlank String username,
@NotBlank String empNo,
@NotBlank String deptCode,
boolean canReadContents
) {
}

View File

@@ -0,0 +1,9 @@
package com.cloudhandson.vpdbackoffice.domain.user;
public record UserRoleView(
long userId,
String username,
long roleId,
String roleName
) {
}

View File

@@ -3,6 +3,7 @@ package com.cloudhandson.vpdbackoffice.mapper;
import com.cloudhandson.vpdbackoffice.domain.permission.AppRole;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionRule;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionView;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -14,6 +15,8 @@ public interface PermissionMapper {
AppRole findRole(@Param("roleId") long roleId);
List<PermissionView> findPermissionViews();
PermissionSet findPermissionSet(@Param("roleId") long roleId, @Param("objectId") long objectId);
Long findPermissionId(@Param("roleId") long roleId, @Param("objectId") long objectId);
@@ -33,6 +36,8 @@ public interface PermissionMapper {
void insertVisibleColumn(@Param("permissionId") long permissionId, @Param("columnName") String columnName);
int deletePermission(@Param("permissionId") long permissionId);
long nextPermissionId();
long nextRuleId();

View File

@@ -1,6 +1,7 @@
package com.cloudhandson.vpdbackoffice.mapper;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
@@ -14,4 +15,17 @@ public interface ProtectedObjectMapper {
ProtectedObject findById(@Param("objectId") long objectId);
List<ProtectedColumn> findColumns(@Param("objectId") long objectId);
long nextObjectId();
long nextColumnId();
void insertObject(@Param("objectId") long objectId, @Param("command") ProtectedObjectCreateCommand command);
void insertColumn(@Param("columnId") long columnId,
@Param("objectId") long objectId,
@Param("columnName") String columnName,
@Param("sensitiveYn") String sensitiveYn);
int disableObject(@Param("objectId") long objectId);
}

View File

@@ -1,6 +1,8 @@
package com.cloudhandson.vpdbackoffice.mapper;
import com.cloudhandson.vpdbackoffice.domain.user.AppUser;
import com.cloudhandson.vpdbackoffice.domain.user.UserCreateCommand;
import com.cloudhandson.vpdbackoffice.domain.user.UserRoleView;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@@ -10,5 +12,17 @@ public interface UserMapper {
List<AppUser> findAll();
List<UserRoleView> findUserRoles();
AppUser findById(@Param("userId") long userId);
long nextUserId();
void insertUser(@Param("userId") long userId, @Param("command") UserCreateCommand command);
int updateActive(@Param("userId") long userId, @Param("activeYn") String activeYn);
void insertUserRole(@Param("userId") long userId, @Param("roleId") long roleId);
int deleteUserRole(@Param("userId") long userId, @Param("roleId") long roleId);
}

View File

@@ -5,6 +5,7 @@ import com.cloudhandson.vpdbackoffice.domain.permission.AppRole;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionRule;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSetCommand;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionView;
import com.cloudhandson.vpdbackoffice.domain.permission.RuleCommand;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
import com.cloudhandson.vpdbackoffice.mapper.PermissionMapper;
@@ -38,6 +39,10 @@ public class PermissionService {
return permissionMapper.findRoles();
}
public List<PermissionView> findPermissionViews() {
return permissionMapper.findPermissionViews();
}
@Transactional
public PermissionSet savePermissionSet(PermissionSetCommand command) {
if (!"SELECT".equalsIgnoreCase(command.action())) {
@@ -83,6 +88,18 @@ public class PermissionService {
return new PermissionSet(permissionId, command.roleId(), command.objectId(), "SELECT", List.of(), List.of());
}
@Transactional
public void deletePermission(long permissionId) {
permissionMapper.deleteRules(permissionId);
permissionMapper.deleteVisibleColumns(permissionId);
int deleted = permissionMapper.deletePermission(permissionId);
if (deleted == 0) {
throw new AppException("삭제할 권한을 찾을 수 없습니다.");
}
auditService.record(new AuditEvent("PERMISSION_DELETED", null, null, "SUCCESS", null, null,
"permissionId=" + permissionId));
}
private void validateRules(List<RuleCommand> rules) {
if (rules == null || rules.isEmpty()) {
throw new AppException("행 규칙은 하나 이상 필요합니다.");

View File

@@ -1,18 +1,27 @@
package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand;
import com.cloudhandson.vpdbackoffice.mapper.ProtectedObjectMapper;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ProtectedObjectService {
private final ProtectedObjectMapper mapper;
private final AuditService auditService;
public ProtectedObjectService(ProtectedObjectMapper mapper) {
public ProtectedObjectService(ProtectedObjectMapper mapper, AuditService auditService) {
this.mapper = mapper;
this.auditService = auditService;
}
public List<ProtectedObject> findEnabled() {
@@ -30,4 +39,38 @@ public class ProtectedObjectService {
public List<ProtectedColumn> findColumns(long objectId) {
return mapper.findColumns(objectId);
}
@Transactional
public void createObject(ProtectedObjectCreateCommand command) {
long objectId = mapper.nextObjectId();
mapper.insertObject(objectId, command);
Set<String> sensitive = splitCsv(command.sensitiveColumns());
for (String column : splitCsv(command.columns())) {
mapper.insertColumn(mapper.nextColumnId(), objectId, column, sensitive.contains(column) ? "Y" : "N");
}
auditService.record(new AuditEvent("PROTECTED_OBJECT_CREATED", null, objectId, "SUCCESS", null, null,
command.objectName()));
}
@Transactional
public void disableObject(long objectId) {
int updated = mapper.disableObject(objectId);
if (updated == 0) {
throw new AppException("보호 객체를 찾을 수 없습니다.");
}
auditService.record(new AuditEvent("PROTECTED_OBJECT_DISABLED", null, objectId, "SUCCESS", null, null, null));
}
private Set<String> splitCsv(String value) {
Set<String> result = new HashSet<>();
if (value == null || value.isBlank()) {
return result;
}
Arrays.stream(value.split(","))
.map(String::trim)
.filter(token -> !token.isBlank())
.map(token -> token.toUpperCase(Locale.ROOT))
.forEach(result::add);
return result;
}
}

View File

@@ -0,0 +1,64 @@
package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent;
import com.cloudhandson.vpdbackoffice.domain.user.AppUser;
import com.cloudhandson.vpdbackoffice.domain.user.UserCreateCommand;
import com.cloudhandson.vpdbackoffice.domain.user.UserRoleView;
import com.cloudhandson.vpdbackoffice.mapper.UserMapper;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class UserService {
private final UserMapper userMapper;
private final AuditService auditService;
public UserService(UserMapper userMapper, AuditService auditService) {
this.userMapper = userMapper;
this.auditService = auditService;
}
public List<AppUser> findAll() {
return userMapper.findAll();
}
public List<UserRoleView> findUserRoles() {
return userMapper.findUserRoles();
}
@Transactional
public void createUser(UserCreateCommand command) {
long userId = userMapper.nextUserId();
userMapper.insertUser(userId, command);
auditService.record(new AuditEvent("USER_CREATED", null, null, "SUCCESS", null, null, command.username()));
}
@Transactional
public void setActive(long userId, boolean active) {
int updated = userMapper.updateActive(userId, active ? "Y" : "N");
if (updated == 0) {
throw new AppException("사용자를 찾을 수 없습니다.");
}
auditService.record(new AuditEvent("USER_ACTIVE_CHANGED", null, null, "SUCCESS", null, null,
"userId=" + userId + ",active=" + active));
}
@Transactional
public void grantRole(long userId, long roleId) {
userMapper.insertUserRole(userId, roleId);
auditService.record(new AuditEvent("USER_ROLE_GRANTED", null, null, "SUCCESS", null, null,
"userId=" + userId + ",roleId=" + roleId));
}
@Transactional
public void revokeRole(long userId, long roleId) {
int deleted = userMapper.deleteUserRole(userId, roleId);
if (deleted == 0) {
throw new AppException("삭제할 사용자 역할 매핑을 찾을 수 없습니다.");
}
auditService.record(new AuditEvent("USER_ROLE_REVOKED", null, null, "SUCCESS", null, null,
"userId=" + userId + ",roleId=" + roleId));
}
}

View File

@@ -30,6 +30,7 @@ public class PermissionController {
public String permissions(Model model) {
model.addAttribute("roles", permissionService.findRoles());
model.addAttribute("objects", protectedObjectService.findEnabled());
model.addAttribute("permissions", permissionService.findPermissionViews());
return "permissions";
}
@@ -52,4 +53,11 @@ public class PermissionController {
redirectAttributes.addFlashAttribute("message", "권한을 저장했습니다.");
return "redirect:/permissions";
}
@PostMapping("/permissions/delete")
public String delete(@RequestParam long permissionId, RedirectAttributes redirectAttributes) {
permissionService.deletePermission(permissionId);
redirectAttributes.addFlashAttribute("message", "권한을 삭제했습니다.");
return "redirect:/permissions";
}
}

View File

@@ -0,0 +1,48 @@
package com.cloudhandson.vpdbackoffice.web;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand;
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class ProtectedObjectController {
private final ProtectedObjectService protectedObjectService;
public ProtectedObjectController(ProtectedObjectService protectedObjectService) {
this.protectedObjectService = protectedObjectService;
}
@GetMapping("/objects")
public String objects(Model model) {
model.addAttribute("objects", protectedObjectService.findEnabled());
return "objects";
}
@PostMapping("/objects")
public String create(
@RequestParam String owner,
@RequestParam String objectName,
@RequestParam String ordsPath,
@RequestParam(required = false) String columns,
@RequestParam(required = false) String sensitiveColumns,
RedirectAttributes redirectAttributes
) {
protectedObjectService.createObject(
new ProtectedObjectCreateCommand(owner, objectName, ordsPath, columns, sensitiveColumns));
redirectAttributes.addFlashAttribute("message", "보호 객체를 추가했습니다.");
return "redirect:/objects";
}
@PostMapping("/objects/disable")
public String disable(@RequestParam long objectId, RedirectAttributes redirectAttributes) {
protectedObjectService.disableObject(objectId);
redirectAttributes.addFlashAttribute("message", "보호 객체를 비활성화했습니다.");
return "redirect:/objects";
}
}

View File

@@ -0,0 +1,77 @@
package com.cloudhandson.vpdbackoffice.web;
import com.cloudhandson.vpdbackoffice.domain.user.UserCreateCommand;
import com.cloudhandson.vpdbackoffice.service.PermissionService;
import com.cloudhandson.vpdbackoffice.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
@Controller
public class UserController {
private final UserService userService;
private final PermissionService permissionService;
public UserController(UserService userService, PermissionService permissionService) {
this.userService = userService;
this.permissionService = permissionService;
}
@GetMapping("/users")
public String users(Model model) {
model.addAttribute("users", userService.findAll());
model.addAttribute("roles", permissionService.findRoles());
model.addAttribute("userRoles", userService.findUserRoles());
return "users";
}
@PostMapping("/users")
public String create(
@RequestParam String username,
@RequestParam String empNo,
@RequestParam String deptCode,
@RequestParam(defaultValue = "false") boolean canReadContents,
RedirectAttributes redirectAttributes
) {
userService.createUser(new UserCreateCommand(username, empNo, deptCode, canReadContents));
redirectAttributes.addFlashAttribute("message", "사용자를 추가했습니다.");
return "redirect:/users";
}
@PostMapping("/users/active")
public String active(
@RequestParam long userId,
@RequestParam boolean active,
RedirectAttributes redirectAttributes
) {
userService.setActive(userId, active);
redirectAttributes.addFlashAttribute("message", "사용자 상태를 변경했습니다.");
return "redirect:/users";
}
@PostMapping("/users/roles")
public String grantRole(
@RequestParam long userId,
@RequestParam long roleId,
RedirectAttributes redirectAttributes
) {
userService.grantRole(userId, roleId);
redirectAttributes.addFlashAttribute("message", "역할을 부여했습니다.");
return "redirect:/users";
}
@PostMapping("/users/roles/delete")
public String revokeRole(
@RequestParam long userId,
@RequestParam long roleId,
RedirectAttributes redirectAttributes
) {
userService.revokeRole(userId, roleId);
redirectAttributes.addFlashAttribute("message", "역할을 해제했습니다.");
return "redirect:/users";
}
}