From 97bb0357f9b62bfe4dc9e58cbe8f429a070effab Mon Sep 17 00:00:00 2001 From: devmrko Date: Tue, 23 Jun 2026 13:52:50 +0900 Subject: [PATCH] [Developer] #424 improve role permission probe workflows Refs #424 --- .../domain/probe/ProbeCommand.java | 1 - .../protectedobject/DatabaseObjectOption.java | 16 +++++ .../mapper/BearerTokenMapper.java | 2 + .../mapper/PermissionMapper.java | 8 +++ .../mapper/ProtectedObjectMapper.java | 7 +++ .../service/BearerTokenService.java | 7 +++ .../service/OrdsProbeService.java | 14 ++--- .../service/PermissionService.java | 19 ++++++ .../service/ProtectedObjectService.java | 36 +++++++++++ .../web/PermissionController.java | 22 ++++++- .../vpdbackoffice/web/ProbeController.java | 8 +-- .../vpdbackoffice/web/RoleController.java | 43 +++++++++++++ .../resources/mapper/BearerTokenMapper.xml | 8 +++ .../resources/mapper/PermissionMapper.xml | 14 +++++ .../mapper/ProtectedObjectMapper.xml | 25 ++++++++ src/main/resources/static/css/app.css | 5 ++ src/main/resources/static/js/app.js | 31 ++++++++++ .../resources/templates/fragments/layout.html | 1 + src/main/resources/templates/permissions.html | 17 +++-- src/main/resources/templates/probe.html | 12 ++-- src/main/resources/templates/roles.html | 62 +++++++++++++++++++ src/main/resources/templates/users.html | 48 +++++++------- .../service/PermissionServiceTest.java | 14 +++++ 23 files changed, 368 insertions(+), 52 deletions(-) create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/protectedobject/DatabaseObjectOption.java create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/web/RoleController.java create mode 100644 src/main/resources/templates/roles.html diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeCommand.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeCommand.java index 3fab295..90b69a7 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeCommand.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/probe/ProbeCommand.java @@ -6,7 +6,6 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Positive; public record ProbeCommand( - @Positive long keyId, @Positive long objectId, @NotBlank String bearerToken, @Min(1) @Max(500) int limit diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/protectedobject/DatabaseObjectOption.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/protectedobject/DatabaseObjectOption.java new file mode 100644 index 0000000..0a4a17d --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/protectedobject/DatabaseObjectOption.java @@ -0,0 +1,16 @@ +package com.cloudhandson.vpdbackoffice.domain.protectedobject; + +public record DatabaseObjectOption( + String owner, + String objectName, + String objectType +) { + + public String value() { + return owner + "." + objectName; + } + + public String label() { + return owner + "." + objectName + " (" + objectType + ")"; + } +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/BearerTokenMapper.java b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/BearerTokenMapper.java index 6eddabe..6d1a807 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/BearerTokenMapper.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/BearerTokenMapper.java @@ -13,6 +13,8 @@ public interface BearerTokenMapper { BearerTokenRecord findById(@Param("keyId") long keyId); + BearerTokenRecord findByHash(@Param("keyHash") String keyHash); + long nextKeyId(); void insertToken(BearerTokenRecord token); diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/PermissionMapper.java b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/PermissionMapper.java index 69e14e8..c5341e6 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/PermissionMapper.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/PermissionMapper.java @@ -15,6 +15,14 @@ public interface PermissionMapper { AppRole findRole(@Param("roleId") long roleId); + long nextRoleId(); + + void insertRole(@Param("roleId") long roleId, + @Param("roleName") String roleName, + @Param("description") String description); + + int deleteRole(@Param("roleId") long roleId); + List findPermissionViews(); PermissionSet findPermissionSet(@Param("roleId") long roleId, @Param("objectId") long objectId); diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java index 7568ec9..fd9111f 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/ProtectedObjectMapper.java @@ -1,5 +1,6 @@ package com.cloudhandson.vpdbackoffice.mapper; +import com.cloudhandson.vpdbackoffice.domain.protectedobject.DatabaseObjectOption; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject; @@ -14,8 +15,14 @@ public interface ProtectedObjectMapper { ProtectedObject findById(@Param("objectId") long objectId); + ProtectedObject findByOwnerAndName(@Param("owner") String owner, @Param("objectName") String objectName); + List findColumns(@Param("objectId") long objectId); + List findDatabaseObjects(); + + List findDatabaseColumns(@Param("owner") String owner, @Param("objectName") String objectName); + long nextObjectId(); long nextColumnId(); diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/BearerTokenService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/BearerTokenService.java index 8542fd2..fcb1547 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/BearerTokenService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/BearerTokenService.java @@ -53,6 +53,13 @@ public class BearerTokenService { return tokenMapper.findById(keyId); } + public BearerTokenRecord findByPlainToken(String plainToken) { + if (plainToken == null || plainToken.isBlank()) { + return null; + } + return tokenMapper.findByHash(tokenHasher.sha256(plainToken)); + } + public boolean matches(BearerTokenRecord record, String plainToken) { if (record == null || plainToken == null || plainToken.isBlank()) { return false; diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java index 2ea6030..a998fe8 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/OrdsProbeService.java @@ -75,7 +75,7 @@ public class OrdsProbeService { )); } - BearerTokenRecord token = tokenService.findById(command.keyId()); + BearerTokenRecord token = tokenService.findByPlainToken(command.bearerToken()); if (token == null) { return auditAndReturn(command, ProbeResult.blocked( ProbeStatus.TOKEN_NOT_FOUND, "TOKEN_NOT_FOUND", "토큰을 찾을 수 없습니다.")); @@ -84,11 +84,6 @@ public class OrdsProbeService { return auditAndReturn(command, ProbeResult.blocked( ProbeStatus.TOKEN_INACTIVE, "TOKEN_INACTIVE", "만료되었거나 회수된 토큰입니다.")); } - if (!tokenService.matches(token, command.bearerToken())) { - return auditAndReturn(command, ProbeResult.blocked( - ProbeStatus.INVALID_TOKEN, "INVALID_TOKEN", "입력한 Bearer Token이 선택한 key와 일치하지 않습니다.")); - } - ProtectedObject object; try { object = protectedObjectService.assertEnabled(command.objectId()); @@ -227,7 +222,7 @@ public class OrdsProbeService { private ProbeResult auditAndReturn(ProbeCommand command, ProbeResult result) { auditService.record(new AuditEvent( "ORDS_PROBE", - command.keyId(), + tokenKeyId(command), command.objectId(), result.status().name(), result.rowCount(), @@ -237,6 +232,11 @@ public class OrdsProbeService { return result; } + private Long tokenKeyId(ProbeCommand command) { + BearerTokenRecord token = tokenService.findByPlainToken(command.bearerToken()); + return token == null ? null : token.keyId(); + } + private ProbeStatus classifyResourceAccess(ResourceAccessException exception) { if (errorClassifier.isTimeout(exception)) { return ProbeStatus.ORDS_TIMEOUT; diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/PermissionService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/PermissionService.java index f3abc82..201f749 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/PermissionService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/PermissionService.java @@ -43,6 +43,25 @@ public class PermissionService { return permissionMapper.findPermissionViews(); } + @Transactional + public void createRole(String roleName, String description) { + if (roleName == null || roleName.isBlank()) { + throw new AppException("역할명은 필수입니다."); + } + long roleId = permissionMapper.nextRoleId(); + permissionMapper.insertRole(roleId, roleName.trim(), description); + auditService.record(new AuditEvent("ROLE_CREATED", null, null, "SUCCESS", null, null, roleName)); + } + + @Transactional + public void deleteRole(long roleId) { + int deleted = permissionMapper.deleteRole(roleId); + if (deleted == 0) { + throw new AppException("삭제할 역할을 찾을 수 없습니다."); + } + auditService.record(new AuditEvent("ROLE_DELETED", null, null, "SUCCESS", null, null, "roleId=" + roleId)); + } + @Transactional public PermissionSet savePermissionSet(PermissionSetCommand command) { if (!"SELECT".equalsIgnoreCase(command.action())) { diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java index 7b257d4..4c15fa8 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/ProtectedObjectService.java @@ -1,6 +1,7 @@ package com.cloudhandson.vpdbackoffice.service; import com.cloudhandson.vpdbackoffice.domain.audit.AuditEvent; +import com.cloudhandson.vpdbackoffice.domain.protectedobject.DatabaseObjectOption; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject; import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand; @@ -28,6 +29,10 @@ public class ProtectedObjectService { return mapper.findEnabled(); } + public List findDatabaseObjects() { + return mapper.findDatabaseObjects(); + } + public ProtectedObject assertEnabled(long objectId) { ProtectedObject object = mapper.findById(objectId); if (object == null || !object.enabled()) { @@ -52,6 +57,37 @@ public class ProtectedObjectService { command.objectName())); } + @Transactional + public ProtectedObject ensureProtectedObject(String owner, String objectName) { + ProtectedObject existing = mapper.findByOwnerAndName(owner, objectName); + if (existing != null) { + return existing; + } + List columns = mapper.findDatabaseColumns(owner, objectName); + if (columns.isEmpty()) { + throw new AppException("DB 객체 컬럼을 찾을 수 없습니다: " + owner + "." + objectName); + } + long objectId = mapper.nextObjectId(); + mapper.insertObject(objectId, new ProtectedObjectCreateCommand( + owner, + objectName, + defaultOrdsPath(owner, objectName), + String.join(",", columns), + "" + )); + for (String column : columns) { + mapper.insertColumn(mapper.nextColumnId(), objectId, column, "N"); + } + auditService.record(new AuditEvent("PROTECTED_OBJECT_CREATED", null, objectId, "SUCCESS", null, null, + owner + "." + objectName)); + return mapper.findById(objectId); + } + + private String defaultOrdsPath(String owner, String objectName) { + String schemaPath = owner.equalsIgnoreCase("CB_ORDS") ? "cb-ords" : owner.toLowerCase(Locale.ROOT); + return schemaPath + "/" + objectName.toLowerCase(Locale.ROOT); + } + @Transactional public void disableObject(long objectId) { int updated = mapper.disableObject(objectId); diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java index 5130b61..e66516e 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/PermissionController.java @@ -31,6 +31,7 @@ public class PermissionController { public String permissions(Model model) { model.addAttribute("roles", permissionService.findRoles()); model.addAttribute("objects", protectedObjectService.findEnabled()); + model.addAttribute("dbObjects", protectedObjectService.findDatabaseObjects()); model.addAttribute("permissions", permissionService.findPermissionViews()); return "permissions"; } @@ -38,12 +39,13 @@ public class PermissionController { @PostMapping("/permissions") public String save( @RequestParam long roleId, - @RequestParam long objectId, + @RequestParam String objectRef, @RequestParam String ruleType, @RequestParam(required = false) String ruleValue, @RequestParam(required = false) String visibleColumns, RedirectAttributes redirectAttributes ) { + long objectId = resolveObjectId(objectRef); permissionService.savePermissionSet(new PermissionSetCommand( roleId, objectId, @@ -55,6 +57,24 @@ public class PermissionController { return "redirect:/permissions"; } + private long resolveObjectId(String objectRef) { + if (objectRef == null || objectRef.isBlank()) { + throw new IllegalArgumentException("보호 객체를 선택하세요."); + } + if (objectRef.startsWith("protected:")) { + return Long.parseLong(objectRef.substring("protected:".length())); + } + if (objectRef.startsWith("db:")) { + String value = objectRef.substring("db:".length()); + int dot = value.indexOf('.'); + if (dot < 1 || dot == value.length() - 1) { + throw new IllegalArgumentException("DB 객체 형식이 올바르지 않습니다."); + } + return protectedObjectService.ensureProtectedObject(value.substring(0, dot), value.substring(dot + 1)).objectId(); + } + throw new IllegalArgumentException("보호 객체 형식이 올바르지 않습니다."); + } + private List splitColumns(String visibleColumns) { if (visibleColumns == null || visibleColumns.isBlank()) { return List.of(); diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/ProbeController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/ProbeController.java index 95cfdbd..96d956c 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/ProbeController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/ProbeController.java @@ -1,7 +1,6 @@ package com.cloudhandson.vpdbackoffice.web; import com.cloudhandson.vpdbackoffice.domain.probe.ProbeCommand; -import com.cloudhandson.vpdbackoffice.service.BearerTokenService; import com.cloudhandson.vpdbackoffice.service.OrdsProbeService; import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService; import org.springframework.stereotype.Controller; @@ -14,35 +13,30 @@ import org.springframework.web.bind.annotation.RequestParam; public class ProbeController { private final OrdsProbeService probeService; - private final BearerTokenService tokenService; private final ProtectedObjectService protectedObjectService; public ProbeController( OrdsProbeService probeService, - BearerTokenService tokenService, ProtectedObjectService protectedObjectService ) { this.probeService = probeService; - this.tokenService = tokenService; this.protectedObjectService = protectedObjectService; } @GetMapping("/probe") public String probe(Model model) { - model.addAttribute("tokens", tokenService.findAll()); model.addAttribute("objects", protectedObjectService.findEnabled()); return "probe"; } @PostMapping("/probe") public String run( - @RequestParam long keyId, @RequestParam long objectId, @RequestParam String bearerToken, @RequestParam(defaultValue = "50") int limit, Model model ) { - model.addAttribute("result", probeService.runProbe(new ProbeCommand(keyId, objectId, bearerToken, limit))); + model.addAttribute("result", probeService.runProbe(new ProbeCommand(objectId, bearerToken, limit))); return "fragments/probe-result :: result"; } } diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/RoleController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/RoleController.java new file mode 100644 index 0000000..05d7f1e --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/RoleController.java @@ -0,0 +1,43 @@ +package com.cloudhandson.vpdbackoffice.web; + +import com.cloudhandson.vpdbackoffice.service.PermissionService; +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 RoleController { + + private final PermissionService permissionService; + + public RoleController(PermissionService permissionService) { + this.permissionService = permissionService; + } + + @GetMapping("/roles") + public String roles(Model model) { + model.addAttribute("roles", permissionService.findRoles()); + return "roles"; + } + + @PostMapping("/roles") + public String create( + @RequestParam String roleName, + @RequestParam(required = false) String description, + RedirectAttributes redirectAttributes + ) { + permissionService.createRole(roleName, description); + redirectAttributes.addFlashAttribute("message", "역할을 추가했습니다."); + return "redirect:/roles"; + } + + @PostMapping("/roles/delete") + public String delete(@RequestParam long roleId, RedirectAttributes redirectAttributes) { + permissionService.deleteRole(roleId); + redirectAttributes.addFlashAttribute("message", "역할을 삭제했습니다."); + return "redirect:/roles"; + } +} diff --git a/src/main/resources/mapper/BearerTokenMapper.xml b/src/main/resources/mapper/BearerTokenMapper.xml index 2bf8847..565d9a5 100644 --- a/src/main/resources/mapper/BearerTokenMapper.xml +++ b/src/main/resources/mapper/BearerTokenMapper.xml @@ -18,6 +18,14 @@ WHERE k.key_id = #{keyId} + + diff --git a/src/main/resources/mapper/PermissionMapper.xml b/src/main/resources/mapper/PermissionMapper.xml index 794c38d..7740b52 100644 --- a/src/main/resources/mapper/PermissionMapper.xml +++ b/src/main/resources/mapper/PermissionMapper.xml @@ -14,6 +14,20 @@ WHERE role_id = #{roleId} + + + + INSERT INTO cb_app_role (role_id, role_name) + VALUES (#{roleId,jdbcType=NUMERIC}, UPPER(#{roleName,jdbcType=VARCHAR})) + + + + DELETE FROM cb_app_role + WHERE role_id = #{roleId,jdbcType=NUMERIC} + + + + + + + + - + @@ -59,7 +68,7 @@ 테이블/뷰 Action 행 규칙 - 표시 컬럼 + NULL 제외 컬럼 diff --git a/src/main/resources/templates/probe.html b/src/main/resources/templates/probe.html index d6213c8..fdab394 100644 --- a/src/main/resources/templates/probe.html +++ b/src/main/resources/templates/probe.html @@ -12,20 +12,16 @@
-
-
-

역할 부여

-
- - - - -
-
+
+

사용자 역할 부여

+
+
+ + + + +
+
-
-

사용자 역할 목록

+

선택 사용자 역할

- +
@@ -101,7 +101,7 @@ - + - + diff --git a/src/test/java/com/cloudhandson/vpdbackoffice/service/PermissionServiceTest.java b/src/test/java/com/cloudhandson/vpdbackoffice/service/PermissionServiceTest.java index b70ad22..c6249c8 100644 --- a/src/test/java/com/cloudhandson/vpdbackoffice/service/PermissionServiceTest.java +++ b/src/test/java/com/cloudhandson/vpdbackoffice/service/PermissionServiceTest.java @@ -88,6 +88,20 @@ class PermissionServiceTest { return roleId == 10L ? new AppRole(10L, "HR_DEPT_ROLE", null) : null; } + @Override + public long nextRoleId() { + return 40L; + } + + @Override + public void insertRole(long roleId, String roleName, String description) { + } + + @Override + public int deleteRole(long roleId) { + return 1; + } + @Override public List findPermissionViews() { return List.of();
사용자
agent_hr HR_DEPT_ROLE @@ -113,7 +113,7 @@
부여된 역할이 없습니다.