@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -5,6 +5,7 @@ public record AppUser(
|
||||
String username,
|
||||
String empNo,
|
||||
String deptCode,
|
||||
String canReadContents,
|
||||
String activeYn
|
||||
) {
|
||||
|
||||
|
||||
@@ -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
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.cloudhandson.vpdbackoffice.domain.user;
|
||||
|
||||
public record UserRoleView(
|
||||
long userId,
|
||||
String username,
|
||||
long roleId,
|
||||
String roleName
|
||||
) {
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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("행 규칙은 하나 이상 필요합니다.");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,23 @@
|
||||
WHERE role_id = #{roleId}
|
||||
</select>
|
||||
|
||||
<select id="findPermissionViews" resultType="com.cloudhandson.vpdbackoffice.domain.permission.PermissionView">
|
||||
SELECT p.perm_id AS permission_id,
|
||||
r.role_id,
|
||||
r.role_name,
|
||||
o.object_id,
|
||||
p.target_name AS object_name,
|
||||
p.action_name AS action,
|
||||
LISTAGG(pr.rule_type || NVL2(pr.rule_value, ':' || pr.rule_value, ''), ', ')
|
||||
WITHIN GROUP (ORDER BY pr.rule_id) AS rules
|
||||
FROM cb_permission p
|
||||
JOIN cb_app_role r ON r.role_id = p.role_id
|
||||
LEFT JOIN cb_protected_object o ON o.object_name = p.target_name
|
||||
LEFT JOIN cb_permission_rule pr ON pr.perm_id = p.perm_id
|
||||
GROUP BY p.perm_id, r.role_id, r.role_name, o.object_id, p.target_name, p.action_name
|
||||
ORDER BY r.role_name, p.target_name
|
||||
</select>
|
||||
|
||||
<select id="findPermissionId" resultType="long">
|
||||
SELECT p.perm_id
|
||||
FROM cb_permission p
|
||||
@@ -73,4 +90,9 @@
|
||||
INSERT INTO cb_permission_column (permission_id, column_name)
|
||||
VALUES (#{permissionId}, #{columnName})
|
||||
</insert>
|
||||
|
||||
<delete id="deletePermission">
|
||||
DELETE FROM cb_permission
|
||||
WHERE perm_id = #{permissionId}
|
||||
</delete>
|
||||
</mapper>
|
||||
|
||||
@@ -21,4 +21,34 @@
|
||||
WHERE object_id = #{objectId}
|
||||
ORDER BY column_id
|
||||
</select>
|
||||
|
||||
<select id="nextObjectId" resultType="long">
|
||||
SELECT NVL(MAX(object_id), 0) + 1 FROM cb_protected_object
|
||||
</select>
|
||||
|
||||
<select id="nextColumnId" resultType="long">
|
||||
SELECT NVL(MAX(column_id), 0) + 1 FROM cb_protected_column
|
||||
</select>
|
||||
|
||||
<insert id="insertObject">
|
||||
INSERT INTO cb_protected_object (object_id, owner, object_name, ords_path, enabled_yn)
|
||||
VALUES (
|
||||
#{objectId},
|
||||
UPPER(#{command.owner}),
|
||||
UPPER(#{command.objectName}),
|
||||
#{command.ordsPath},
|
||||
'Y'
|
||||
)
|
||||
</insert>
|
||||
|
||||
<insert id="insertColumn">
|
||||
INSERT INTO cb_protected_column (column_id, object_id, column_name, sensitive_yn)
|
||||
VALUES (#{columnId}, #{objectId}, UPPER(#{columnName}), #{sensitiveYn})
|
||||
</insert>
|
||||
|
||||
<update id="disableObject">
|
||||
UPDATE cb_protected_object
|
||||
SET enabled_yn = 'N'
|
||||
WHERE object_id = #{objectId}
|
||||
</update>
|
||||
</mapper>
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
user_name AS username,
|
||||
employee_no AS emp_no,
|
||||
dept_code,
|
||||
can_read_contents,
|
||||
active AS active_yn
|
||||
FROM cb_app_user
|
||||
ORDER BY username
|
||||
@@ -17,8 +18,57 @@
|
||||
user_name AS username,
|
||||
employee_no AS emp_no,
|
||||
dept_code,
|
||||
can_read_contents,
|
||||
active AS active_yn
|
||||
FROM cb_app_user
|
||||
WHERE user_id = #{userId}
|
||||
</select>
|
||||
|
||||
<select id="findUserRoles" resultType="com.cloudhandson.vpdbackoffice.domain.user.UserRoleView">
|
||||
SELECT u.user_id,
|
||||
u.user_name AS username,
|
||||
r.role_id,
|
||||
r.role_name
|
||||
FROM cb_user_role ur
|
||||
JOIN cb_app_user u ON u.user_id = ur.user_id
|
||||
JOIN cb_app_role r ON r.role_id = ur.role_id
|
||||
ORDER BY u.user_name, r.role_name
|
||||
</select>
|
||||
|
||||
<select id="nextUserId" resultType="long">
|
||||
SELECT NVL(MAX(user_id), 0) + 1 FROM cb_app_user
|
||||
</select>
|
||||
|
||||
<insert id="insertUser">
|
||||
INSERT INTO cb_app_user (
|
||||
user_id, user_name, employee_no, dept_code, can_read_contents, active
|
||||
) VALUES (
|
||||
#{userId},
|
||||
#{command.username},
|
||||
#{command.empNo},
|
||||
#{command.deptCode},
|
||||
<choose>
|
||||
<when test="command.canReadContents"> 'Y' </when>
|
||||
<otherwise> 'N' </otherwise>
|
||||
</choose>,
|
||||
'Y'
|
||||
)
|
||||
</insert>
|
||||
|
||||
<update id="updateActive">
|
||||
UPDATE cb_app_user
|
||||
SET active = #{activeYn}
|
||||
WHERE user_id = #{userId}
|
||||
</update>
|
||||
|
||||
<insert id="insertUserRole">
|
||||
INSERT INTO cb_user_role (user_id, role_id)
|
||||
VALUES (#{userId}, #{roleId})
|
||||
</insert>
|
||||
|
||||
<delete id="deleteUserRole">
|
||||
DELETE FROM cb_user_role
|
||||
WHERE user_id = #{userId}
|
||||
AND role_id = #{roleId}
|
||||
</delete>
|
||||
</mapper>
|
||||
|
||||
@@ -86,6 +86,14 @@ body {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.switch-field {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
min-height: 38px;
|
||||
padding-left: 2.5rem;
|
||||
}
|
||||
|
||||
.token-value {
|
||||
display: block;
|
||||
margin-top: .5rem;
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="/">VPD Backoffice</a>
|
||||
<div class="navbar-nav">
|
||||
<a class="nav-link" href="/users">사용자</a>
|
||||
<a class="nav-link" href="/permissions">권한</a>
|
||||
<a class="nav-link" href="/objects">테이블/뷰</a>
|
||||
<a class="nav-link" href="/tokens">토큰</a>
|
||||
<a class="nav-link" href="/probe">ORDS 검증</a>
|
||||
</div>
|
||||
|
||||
78
src/main/resources/templates/objects.html
Normal file
78
src/main/resources/templates/objects.html
Normal file
@@ -0,0 +1,78 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('테이블/뷰 관리')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4">
|
||||
<div class="page-title">
|
||||
<h1>테이블/뷰 관리</h1>
|
||||
<p>권한에 사용할 보호 객체와 ORDS 경로를 등록합니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>테이블/뷰 추가</h2>
|
||||
<form method="post" action="/objects" class="form-grid">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<label>
|
||||
Owner
|
||||
<input class="form-control" name="owner" value="ADMIN" required>
|
||||
</label>
|
||||
<label>
|
||||
Object
|
||||
<input class="form-control" name="objectName" placeholder="CB_V_SEARCH_DOCUMENTS" required>
|
||||
</label>
|
||||
<label>
|
||||
ORDS Path
|
||||
<input class="form-control" name="ordsPath" placeholder="cb-agent-security/vpd/documents" required>
|
||||
</label>
|
||||
<label>
|
||||
컬럼
|
||||
<input class="form-control" name="columns" placeholder="DOC_ID,TITLE,CONTENTS">
|
||||
</label>
|
||||
<label>
|
||||
민감 컬럼
|
||||
<input class="form-control" name="sensitiveColumns" placeholder="CONTENTS">
|
||||
</label>
|
||||
<button class="btn btn-primary" type="submit">추가</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>등록된 테이블/뷰</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Owner</th>
|
||||
<th>Object</th>
|
||||
<th>ORDS Path</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="object : ${objects}">
|
||||
<td th:text="${object.objectId()}">1</td>
|
||||
<td th:text="${object.owner()}">ADMIN</td>
|
||||
<td th:text="${object.objectName()}">CB_V_SEARCH_DOCUMENTS</td>
|
||||
<td><code th:text="${object.ordsPath()}">path</code></td>
|
||||
<td>
|
||||
<form method="post" action="/objects/disable" class="inline-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="objectId" th:value="${object.objectId()}">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">비활성화</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(objects)}">
|
||||
<td colspan="5" class="text-muted">등록된 테이블/뷰가 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,6 +12,7 @@
|
||||
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>권한 추가</h2>
|
||||
<form method="post" action="/permissions" class="form-grid">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<label>
|
||||
@@ -42,6 +43,43 @@
|
||||
<button class="btn btn-primary" type="submit">저장</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>권한 목록</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>역할</th>
|
||||
<th>테이블/뷰</th>
|
||||
<th>Action</th>
|
||||
<th>행 규칙</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="permission : ${permissions}">
|
||||
<td th:text="${permission.permissionId()}">100</td>
|
||||
<td th:text="${permission.roleName()}">HR_DEPT_ROLE</td>
|
||||
<td th:text="${permission.objectName()}">CB_V_SEARCH_DOCUMENTS</td>
|
||||
<td th:text="${permission.action()}">SELECT</td>
|
||||
<td th:text="${permission.rules()} ?: '-'">ALL</td>
|
||||
<td>
|
||||
<form method="post" action="/permissions/delete" class="inline-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="permissionId" th:value="${permission.permissionId()}">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">삭제</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(permissions)}">
|
||||
<td colspan="6" class="text-muted">등록된 권한이 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
131
src/main/resources/templates/users.html
Normal file
131
src/main/resources/templates/users.html
Normal file
@@ -0,0 +1,131 @@
|
||||
<!doctype html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head th:replace="~{fragments/layout :: head('사용자 관리')}"></head>
|
||||
<body>
|
||||
<nav th:replace="~{fragments/layout :: nav}"></nav>
|
||||
<main class="container py-4">
|
||||
<div class="page-title">
|
||||
<h1>사용자 관리</h1>
|
||||
<p>Bearer Token과 권한 매핑에 사용할 내부 사용자를 관리합니다.</p>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>사용자 추가</h2>
|
||||
<form method="post" action="/users" class="form-grid">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<label>
|
||||
사용자명
|
||||
<input class="form-control" name="username" required>
|
||||
</label>
|
||||
<label>
|
||||
사번
|
||||
<input class="form-control" name="empNo" required>
|
||||
</label>
|
||||
<label>
|
||||
부서코드
|
||||
<input class="form-control" name="deptCode" required>
|
||||
</label>
|
||||
<label class="form-check form-switch switch-field">
|
||||
<input class="form-check-input" name="canReadContents" type="checkbox" value="true">
|
||||
<span class="form-check-label">민감 컬럼 표시</span>
|
||||
</label>
|
||||
<button class="btn btn-primary" type="submit">추가</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>사용자 목록</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>사용자명</th>
|
||||
<th>사번</th>
|
||||
<th>부서</th>
|
||||
<th>민감 컬럼</th>
|
||||
<th>상태</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="user : ${users}">
|
||||
<td th:text="${user.userId()}">1</td>
|
||||
<td th:text="${user.username()}">agent_hr</td>
|
||||
<td th:text="${user.empNo()}">E10234</td>
|
||||
<td th:text="${user.deptCode()}">HR</td>
|
||||
<td><span class="badge" th:classappend="${user.canReadContents() == 'Y'} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${user.canReadContents()}">N</span></td>
|
||||
<td><span class="badge" th:classappend="${user.active()} ? ' text-bg-success' : ' text-bg-secondary'" th:text="${user.activeYn()}">Y</span></td>
|
||||
<td>
|
||||
<form method="post" action="/users/active" class="inline-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="userId" th:value="${user.userId()}">
|
||||
<input type="hidden" name="active" th:value="${!user.active()}">
|
||||
<button class="btn btn-sm btn-outline-secondary" type="submit" th:text="${user.active()} ? '비활성화' : '활성화'">변경</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(users)}">
|
||||
<td colspan="7" class="text-muted">등록된 사용자가 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>역할 부여</h2>
|
||||
<form method="post" action="/users/roles" class="form-grid">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<label>
|
||||
사용자
|
||||
<select class="form-select" name="userId" required>
|
||||
<option th:each="user : ${users}" th:value="${user.userId()}" th:text="${user.username()}"></option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
역할
|
||||
<select class="form-select" name="roleId" required>
|
||||
<option th:each="role : ${roles}" th:value="${role.roleId()}" th:text="${role.roleName()}"></option>
|
||||
</select>
|
||||
</label>
|
||||
<button class="btn btn-primary" type="submit">부여</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<section class="content-band">
|
||||
<h2>사용자 역할 목록</h2>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>사용자</th>
|
||||
<th>역할</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="mapping : ${userRoles}">
|
||||
<td th:text="${mapping.username()}">agent_hr</td>
|
||||
<td th:text="${mapping.roleName()}">HR_DEPT_ROLE</td>
|
||||
<td>
|
||||
<form method="post" action="/users/roles/delete" class="inline-form">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<input type="hidden" name="userId" th:value="${mapping.userId()}">
|
||||
<input type="hidden" name="roleId" th:value="${mapping.roleId()}">
|
||||
<button class="btn btn-sm btn-outline-danger" type="submit">해제</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(userRoles)}">
|
||||
<td colspan="3" class="text-muted">부여된 역할이 없습니다.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -7,6 +7,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.domain.protectedobject.ProtectedObject;
|
||||
@@ -25,7 +26,8 @@ class PermissionServiceTest {
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
permissionMapper = new FakePermissionMapper();
|
||||
protectedObjectService = new ProtectedObjectService(null) {
|
||||
AuditService auditService = new AuditService(new NoopAuditMapper());
|
||||
protectedObjectService = new ProtectedObjectService(null, auditService) {
|
||||
@Override
|
||||
public ProtectedObject assertEnabled(long objectId) {
|
||||
return new ProtectedObject(1L, "ADMIN", "CB_V_SEARCH_DOCUMENTS", "cb-agent-security/vpd/documents", "Y");
|
||||
@@ -36,7 +38,6 @@ class PermissionServiceTest {
|
||||
return List.of();
|
||||
}
|
||||
};
|
||||
AuditService auditService = new AuditService(new NoopAuditMapper());
|
||||
permissionService = new PermissionService(permissionMapper, protectedObjectService, auditService);
|
||||
}
|
||||
|
||||
@@ -87,6 +88,11 @@ class PermissionServiceTest {
|
||||
return roleId == 10L ? new AppRole(10L, "HR_DEPT_ROLE", null) : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<PermissionView> findPermissionViews() {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PermissionSet findPermissionSet(long roleId, long objectId) {
|
||||
return null;
|
||||
@@ -121,6 +127,11 @@ class PermissionServiceTest {
|
||||
public void insertVisibleColumn(long permissionId, String columnName) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int deletePermission(long permissionId) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long nextPermissionId() {
|
||||
return 1000L;
|
||||
|
||||
Reference in New Issue
Block a user