From f98729aa78054609c96311edb03319e58675542e Mon Sep 17 00:00:00 2001 From: devmrko Date: Thu, 25 Jun 2026 17:16:15 +0900 Subject: [PATCH] fix #424: add schema bulk VPD policy apply --- .../domain/vpd/VpdBulkApplyResult.java | 14 ++ .../domain/vpd/VpdPolicyFormOptions.java | 1 + .../domain/vpd/VpdSchemaObjectOption.java | 8 ++ .../vpdbackoffice/mapper/VpdPolicyMapper.java | 15 +++ .../service/VpdPolicyService.java | 122 +++++++++++++++++- .../web/VpdPolicyController.java | 37 ++++++ src/main/resources/mapper/VpdPolicyMapper.xml | 34 +++++ .../resources/templates/vpd-policies.html | 101 +++++++++++++++ 8 files changed, 330 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdBulkApplyResult.java create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdSchemaObjectOption.java diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdBulkApplyResult.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdBulkApplyResult.java new file mode 100644 index 0000000..a145f88 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdBulkApplyResult.java @@ -0,0 +1,14 @@ +package com.cloudhandson.vpdbackoffice.domain.vpd; + +public record VpdBulkApplyResult( + int total, + int created, + int skipped, + int failed +) { + + public String summary() { + return "VPD bulk 적용 완료: 대상 " + total + "개, 등록 " + created + + "개, 건너뜀 " + skipped + "개, 실패 " + failed + "개"; + } +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdPolicyFormOptions.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdPolicyFormOptions.java index 0a5661b..63e1684 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdPolicyFormOptions.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdPolicyFormOptions.java @@ -4,6 +4,7 @@ import java.util.List; public record VpdPolicyFormOptions( List policyNames, + List schemaOwners, List owners, List functions, List statementTypes diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdSchemaObjectOption.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdSchemaObjectOption.java new file mode 100644 index 0000000..55e42ec --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdSchemaObjectOption.java @@ -0,0 +1,8 @@ +package com.cloudhandson.vpdbackoffice.domain.vpd; + +public record VpdSchemaObjectOption( + String owner, + String objectName, + String objectType +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/VpdPolicyMapper.java b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/VpdPolicyMapper.java index 04aad37..e47e32a 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/VpdPolicyMapper.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/VpdPolicyMapper.java @@ -1,6 +1,7 @@ package com.cloudhandson.vpdbackoffice.mapper; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionOption; +import com.cloudhandson.vpdbackoffice.domain.vpd.VpdSchemaObjectOption; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView; import java.util.List; import org.apache.ibatis.annotations.Mapper; @@ -13,16 +14,30 @@ public interface VpdPolicyMapper { List findPolicyNameOptions(); + List findSchemaOwnerOptions(); + List findOwnerOptions(); List findFunctionOptions(); + List findSchemaObjects( + @Param("owner") String owner, + @Param("includeTablesYn") String includeTablesYn, + @Param("includeViewsYn") String includeViewsYn + ); + VpdPolicyView findPolicy( @Param("objectOwner") String objectOwner, @Param("objectName") String objectName, @Param("policyName") String policyName ); + VpdPolicyView findAnyPolicy( + @Param("objectOwner") String objectOwner, + @Param("objectName") String objectName, + @Param("policyName") String policyName + ); + String findFunctionSource( @Param("owner") String owner, @Param("objectName") String objectName, diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java index 31035e0..70c51e7 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java @@ -1,15 +1,18 @@ package com.cloudhandson.vpdbackoffice.service; +import com.cloudhandson.vpdbackoffice.domain.vpd.VpdBulkApplyResult; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionSource; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyCreateCommand; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyDetail; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyExplanation; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyFormOptions; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView; +import com.cloudhandson.vpdbackoffice.domain.vpd.VpdSchemaObjectOption; import com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper; import java.util.List; import java.util.Locale; import java.util.Set; +import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -36,6 +39,7 @@ public class VpdPolicyService { public VpdPolicyFormOptions formOptions() { return new VpdPolicyFormOptions( mapper.findPolicyNameOptions(), + mapper.findSchemaOwnerOptions(), mapper.findOwnerOptions(), mapper.findFunctionOptions(), List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX") @@ -47,10 +51,97 @@ public class VpdPolicyService { List.of(), List.of(), List.of(), + List.of(), List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX") ); } + public VpdBulkApplyResult bulkApplySchema( + String schemaOwner, + boolean includeTables, + boolean includeViews, + String functionKey, + String functionOwnerValue, + String functionNameValue, + String statementTypesValue, + boolean enabled, + boolean updateCheck, + String filterPredicateValue + ) { + String owner = requiredIdentifier(schemaOwner, "Schema"); + if (!includeTables && !includeViews) { + throw new AppException("TABLE 또는 VIEW 중 하나 이상 선택해야 합니다."); + } + List targets = mapper.findSchemaObjects( + owner, + includeTables ? "Y" : "N", + includeViews ? "Y" : "N" + ); + if (targets.isEmpty()) { + throw new AppException("선택한 스키마에서 VPD 적용 대상 TABLE/VIEW를 찾을 수 없습니다: " + owner); + } + + FunctionRef functionRef = parseFunctionRef(functionKey); + String filterPredicate = filterPredicateValue == null ? "" : filterPredicateValue.trim(); + if (functionRef == null && filterPredicate.isBlank()) { + throw new AppException("벌크 적용은 기존 Function을 선택하거나 Filter predicate를 입력해야 합니다."); + } + + String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class); + String functionOwner; + String packageName; + String functionName; + if (functionRef != null) { + functionOwner = functionRef.owner(); + packageName = functionRef.packageName(); + functionName = functionRef.functionName(); + } else { + functionOwner = functionOwnerValue == null || functionOwnerValue.isBlank() + ? currentUser + : requiredIdentifier(functionOwnerValue, "Function owner"); + packageName = null; + functionName = functionNameValue == null || functionNameValue.isBlank() + ? generatedFunctionName(owner + "_BULK_POLICY") + : requiredIdentifier(functionNameValue, "Function name"); + } + + if (!filterPredicate.isBlank()) { + if (currentUser == null || !currentUser.equalsIgnoreCase(functionOwner)) { + throw new AppException("필터 함수 자동 생성은 현재 연결 사용자 스키마에만 가능합니다. 현재 사용자: " + + currentUser + ", Function owner: " + functionOwner); + } + createFilterFunction(functionName, filterPredicate); + } + + String statementTypes = normalizeStatementTypes(statementTypesValue); + int created = 0; + int skipped = 0; + int failed = 0; + for (VpdSchemaObjectOption target : targets) { + String policyName = generatedPolicyName(target.objectName()); + if (mapper.findAnyPolicy(target.owner(), target.objectName(), policyName) != null) { + skipped++; + continue; + } + try { + addPolicy( + target.owner(), + target.objectName(), + policyName, + functionOwner, + packageName == null ? functionName : packageName + "." + functionName, + statementTypes, + enabled, + updateCheck + ); + created++; + } catch (DataAccessException exception) { + failed++; + } + } + return new VpdBulkApplyResult(targets.size(), created, skipped, failed); + } + public VpdFunctionSource findFunctionSource(String owner, String packageName, String functionName) { String normalizedOwner = requiredIdentifier(owner, "Function owner"); String normalizedFunction = requiredIdentifier(functionName, "Function name"); @@ -106,6 +197,28 @@ public class VpdPolicyService { createFilterFunction(functionName, filterPredicate); } + addPolicy( + objectOwner, + objectName, + policyName, + functionOwner, + packageName == null ? functionName : packageName + "." + functionName, + statementTypes, + command.enabled(), + command.updateCheck() + ); + } + + private void addPolicy( + String objectOwner, + String objectName, + String policyName, + String functionOwner, + String policyFunction, + String statementTypes, + boolean enabled, + boolean updateCheck + ) { jdbcTemplate.update(""" BEGIN DBMS_RLS.ADD_POLICY( @@ -120,12 +233,12 @@ public class VpdPolicyService { policy_type => DBMS_RLS.DYNAMIC ); END; - """.formatted(command.updateCheck() ? "TRUE" : "FALSE", command.enabled() ? "TRUE" : "FALSE"), + """.formatted(updateCheck ? "TRUE" : "FALSE", enabled ? "TRUE" : "FALSE"), objectOwner, objectName, policyName, functionOwner, - packageName == null ? functionName : packageName + "." + functionName, + policyFunction, statementTypes); } @@ -301,6 +414,11 @@ public class VpdPolicyService { return generated.length() > 128 ? generated.substring(0, 128) : generated; } + private String generatedPolicyName(String objectName) { + String name = objectName + "_POLICY"; + return name.length() > 128 ? name.substring(0, 128) : name; + } + private FunctionRef parseFunctionRef(String functionKey) { if (functionKey == null || functionKey.isBlank()) { return null; diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java index e702da5..0e17aa6 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java @@ -80,6 +80,43 @@ public class VpdPolicyController { return "redirect:/vpd-policies"; } + @PostMapping("/vpd-policies/bulk") + public String bulkApplyPolicy( + @RequestParam String schemaOwner, + @RequestParam(defaultValue = "false") boolean includeTables, + @RequestParam(defaultValue = "false") boolean includeViews, + @RequestParam(required = false) String functionKey, + @RequestParam(required = false) String functionOwner, + @RequestParam(required = false) String functionName, + @RequestParam(defaultValue = "SELECT") List statementTypes, + @RequestParam(defaultValue = "false") boolean enabled, + @RequestParam(defaultValue = "false") boolean updateCheck, + @RequestParam(required = false) String filterPredicate, + RedirectAttributes redirectAttributes + ) { + try { + var result = vpdPolicyService.bulkApplySchema( + schemaOwner, + includeTables, + includeViews, + functionKey, + functionOwner, + functionName, + String.join(",", statementTypes), + enabled, + updateCheck, + filterPredicate + ); + redirectAttributes.addFlashAttribute("successMessage", result.summary()); + } catch (AppException exception) { + redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage()); + } catch (DataAccessException exception) { + RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception); + redirectAttributes.addFlashAttribute("errorMessage", message.message()); + } + return "redirect:/vpd-policies"; + } + @GetMapping("/vpd-policies/function-source") public String functionSource( @RequestParam String owner, diff --git a/src/main/resources/mapper/VpdPolicyMapper.xml b/src/main/resources/mapper/VpdPolicyMapper.xml index 1abb5fb..4d9073a 100644 --- a/src/main/resources/mapper/VpdPolicyMapper.xml +++ b/src/main/resources/mapper/VpdPolicyMapper.xml @@ -56,6 +56,20 @@ ORDER BY 1 + + + + + + + +
+ Object Types +
+ + +
+
+ + + +
+ Statement Types +
+ +
+
+
+ + +
+
+ + +
+ +
+ + + +
+ + + +

VPD Policies