[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 username,
String empNo, String empNo,
String deptCode, String deptCode,
String canReadContents,
String activeYn 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.AppRole;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionRule; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionRule;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionView;
import java.util.List; import java.util.List;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -14,6 +15,8 @@ public interface PermissionMapper {
AppRole findRole(@Param("roleId") long roleId); AppRole findRole(@Param("roleId") long roleId);
List<PermissionView> findPermissionViews();
PermissionSet findPermissionSet(@Param("roleId") long roleId, @Param("objectId") long objectId); PermissionSet findPermissionSet(@Param("roleId") long roleId, @Param("objectId") long objectId);
Long findPermissionId(@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); void insertVisibleColumn(@Param("permissionId") long permissionId, @Param("columnName") String columnName);
int deletePermission(@Param("permissionId") long permissionId);
long nextPermissionId(); long nextPermissionId();
long nextRuleId(); long nextRuleId();

View File

@@ -1,6 +1,7 @@
package com.cloudhandson.vpdbackoffice.mapper; package com.cloudhandson.vpdbackoffice.mapper;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
import java.util.List; import java.util.List;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@@ -14,4 +15,17 @@ public interface ProtectedObjectMapper {
ProtectedObject findById(@Param("objectId") long objectId); ProtectedObject findById(@Param("objectId") long objectId);
List<ProtectedColumn> findColumns(@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; package com.cloudhandson.vpdbackoffice.mapper;
import com.cloudhandson.vpdbackoffice.domain.user.AppUser; 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 java.util.List;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
@@ -10,5 +12,17 @@ public interface UserMapper {
List<AppUser> findAll(); List<AppUser> findAll();
List<UserRoleView> findUserRoles();
AppUser findById(@Param("userId") long userId); 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.PermissionRule;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSetCommand; 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.permission.RuleCommand;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
import com.cloudhandson.vpdbackoffice.mapper.PermissionMapper; import com.cloudhandson.vpdbackoffice.mapper.PermissionMapper;
@@ -38,6 +39,10 @@ public class PermissionService {
return permissionMapper.findRoles(); return permissionMapper.findRoles();
} }
public List<PermissionView> findPermissionViews() {
return permissionMapper.findPermissionViews();
}
@Transactional @Transactional
public PermissionSet savePermissionSet(PermissionSetCommand command) { public PermissionSet savePermissionSet(PermissionSetCommand command) {
if (!"SELECT".equalsIgnoreCase(command.action())) { 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()); 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) { private void validateRules(List<RuleCommand> rules) {
if (rules == null || rules.isEmpty()) { if (rules == null || rules.isEmpty()) {
throw new AppException("행 규칙은 하나 이상 필요합니다."); throw new AppException("행 규칙은 하나 이상 필요합니다.");

View File

@@ -1,18 +1,27 @@
package com.cloudhandson.vpdbackoffice.service; 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.ProtectedColumn;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand;
import com.cloudhandson.vpdbackoffice.mapper.ProtectedObjectMapper; import com.cloudhandson.vpdbackoffice.mapper.ProtectedObjectMapper;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service @Service
public class ProtectedObjectService { public class ProtectedObjectService {
private final ProtectedObjectMapper mapper; private final ProtectedObjectMapper mapper;
private final AuditService auditService;
public ProtectedObjectService(ProtectedObjectMapper mapper) { public ProtectedObjectService(ProtectedObjectMapper mapper, AuditService auditService) {
this.mapper = mapper; this.mapper = mapper;
this.auditService = auditService;
} }
public List<ProtectedObject> findEnabled() { public List<ProtectedObject> findEnabled() {
@@ -30,4 +39,38 @@ public class ProtectedObjectService {
public List<ProtectedColumn> findColumns(long objectId) { public List<ProtectedColumn> findColumns(long objectId) {
return mapper.findColumns(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) { public String permissions(Model model) {
model.addAttribute("roles", permissionService.findRoles()); model.addAttribute("roles", permissionService.findRoles());
model.addAttribute("objects", protectedObjectService.findEnabled()); model.addAttribute("objects", protectedObjectService.findEnabled());
model.addAttribute("permissions", permissionService.findPermissionViews());
return "permissions"; return "permissions";
} }
@@ -52,4 +53,11 @@ public class PermissionController {
redirectAttributes.addFlashAttribute("message", "권한을 저장했습니다."); redirectAttributes.addFlashAttribute("message", "권한을 저장했습니다.");
return "redirect:/permissions"; 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";
}
}

View File

@@ -14,6 +14,23 @@
WHERE role_id = #{roleId} WHERE role_id = #{roleId}
</select> </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 id="findPermissionId" resultType="long">
SELECT p.perm_id SELECT p.perm_id
FROM cb_permission p FROM cb_permission p
@@ -73,4 +90,9 @@
INSERT INTO cb_permission_column (permission_id, column_name) INSERT INTO cb_permission_column (permission_id, column_name)
VALUES (#{permissionId}, #{columnName}) VALUES (#{permissionId}, #{columnName})
</insert> </insert>
<delete id="deletePermission">
DELETE FROM cb_permission
WHERE perm_id = #{permissionId}
</delete>
</mapper> </mapper>

View File

@@ -21,4 +21,34 @@
WHERE object_id = #{objectId} WHERE object_id = #{objectId}
ORDER BY column_id ORDER BY column_id
</select> </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> </mapper>

View File

@@ -7,6 +7,7 @@
user_name AS username, user_name AS username,
employee_no AS emp_no, employee_no AS emp_no,
dept_code, dept_code,
can_read_contents,
active AS active_yn active AS active_yn
FROM cb_app_user FROM cb_app_user
ORDER BY username ORDER BY username
@@ -17,8 +18,57 @@
user_name AS username, user_name AS username,
employee_no AS emp_no, employee_no AS emp_no,
dept_code, dept_code,
can_read_contents,
active AS active_yn active AS active_yn
FROM cb_app_user FROM cb_app_user
WHERE user_id = #{userId} WHERE user_id = #{userId}
</select> </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> </mapper>

View File

@@ -86,6 +86,14 @@ body {
display: inline; display: inline;
} }
.switch-field {
align-items: center;
display: flex;
gap: .5rem;
min-height: 38px;
padding-left: 2.5rem;
}
.token-value { .token-value {
display: block; display: block;
margin-top: .5rem; margin-top: .5rem;

View File

@@ -15,7 +15,9 @@
<div class="container"> <div class="container">
<a class="navbar-brand" href="/">VPD Backoffice</a> <a class="navbar-brand" href="/">VPD Backoffice</a>
<div class="navbar-nav"> <div class="navbar-nav">
<a class="nav-link" href="/users">사용자</a>
<a class="nav-link" href="/permissions">권한</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="/tokens">토큰</a>
<a class="nav-link" href="/probe">ORDS 검증</a> <a class="nav-link" href="/probe">ORDS 검증</a>
</div> </div>

View 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>

View File

@@ -12,6 +12,7 @@
<div class="alert alert-success" th:if="${message}" th:text="${message}"></div> <div class="alert alert-success" th:if="${message}" th:text="${message}"></div>
<section class="content-band"> <section class="content-band">
<h2>권한 추가</h2>
<form method="post" action="/permissions" class="form-grid"> <form method="post" action="/permissions" class="form-grid">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
<label> <label>
@@ -42,6 +43,43 @@
<button class="btn btn-primary" type="submit">저장</button> <button class="btn btn-primary" type="submit">저장</button>
</form> </form>
</section> </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> </main>
</body> </body>
</html> </html>

View 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>

View File

@@ -7,6 +7,7 @@ import com.cloudhandson.vpdbackoffice.domain.permission.AppRole;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionRule; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionRule;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet; import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSet;
import com.cloudhandson.vpdbackoffice.domain.permission.PermissionSetCommand; 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.permission.RuleCommand;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
@@ -25,7 +26,8 @@ class PermissionServiceTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
permissionMapper = new FakePermissionMapper(); permissionMapper = new FakePermissionMapper();
protectedObjectService = new ProtectedObjectService(null) { AuditService auditService = new AuditService(new NoopAuditMapper());
protectedObjectService = new ProtectedObjectService(null, auditService) {
@Override @Override
public ProtectedObject assertEnabled(long objectId) { public ProtectedObject assertEnabled(long objectId) {
return new ProtectedObject(1L, "ADMIN", "CB_V_SEARCH_DOCUMENTS", "cb-agent-security/vpd/documents", "Y"); return new ProtectedObject(1L, "ADMIN", "CB_V_SEARCH_DOCUMENTS", "cb-agent-security/vpd/documents", "Y");
@@ -36,7 +38,6 @@ class PermissionServiceTest {
return List.of(); return List.of();
} }
}; };
AuditService auditService = new AuditService(new NoopAuditMapper());
permissionService = new PermissionService(permissionMapper, protectedObjectService, auditService); permissionService = new PermissionService(permissionMapper, protectedObjectService, auditService);
} }
@@ -87,6 +88,11 @@ class PermissionServiceTest {
return roleId == 10L ? new AppRole(10L, "HR_DEPT_ROLE", null) : null; return roleId == 10L ? new AppRole(10L, "HR_DEPT_ROLE", null) : null;
} }
@Override
public List<PermissionView> findPermissionViews() {
return List.of();
}
@Override @Override
public PermissionSet findPermissionSet(long roleId, long objectId) { public PermissionSet findPermissionSet(long roleId, long objectId) {
return null; return null;
@@ -121,6 +127,11 @@ class PermissionServiceTest {
public void insertVisibleColumn(long permissionId, String columnName) { public void insertVisibleColumn(long permissionId, String columnName) {
} }
@Override
public int deletePermission(long permissionId) {
return 1;
}
@Override @Override
public long nextPermissionId() { public long nextPermissionId() {
return 1000L; return 1000L;