From 91b03ed952ee3a5f0f7b32dafba1ac4541427d27 Mon Sep 17 00:00:00 2001 From: devmrko Date: Fri, 26 Jun 2026 05:34:56 +0900 Subject: [PATCH] fix #477: list vpd target objects --- .../477-oracle-vpd-ords-control-ux/README.md | 3 +- .../vpdbackoffice/config/DbPoolWarmup.java | 8 ++- .../domain/vpd/VpdTargetView.java | 24 +++++++++ .../vpdbackoffice/mapper/VpdPolicyMapper.java | 3 ++ .../service/VpdPolicyService.java | 38 +++++++++++++- .../web/VpdPolicyController.java | 2 + src/main/resources/mapper/VpdPolicyMapper.xml | 39 ++++++++++++++ src/main/resources/static/css/app.css | 9 +++- .../resources/templates/vpd-policies.html | 51 +++++++++++++++++++ 9 files changed, 173 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdTargetView.java diff --git a/docs/design/477-oracle-vpd-ords-control-ux/README.md b/docs/design/477-oracle-vpd-ords-control-ux/README.md index 4c5841d..bcff085 100644 --- a/docs/design/477-oracle-vpd-ords-control-ux/README.md +++ b/docs/design/477-oracle-vpd-ords-control-ux/README.md @@ -22,4 +22,5 @@ - 권한 관리 화면은 Backoffice Tables 레이어를 강조한다. - VPD 설정 화면은 Oracle Database VPD 레이어를 강조한다. - ORDS Handler 생성과 ORDS 검증 화면은 ORDS 서빙/검증 레이어를 강조한다. -- VPD 적용 대상은 TABLE/VIEW로 표기하고, ORDS path는 서빙/검증 화면에서만 노출한다. +- VPD 적용 대상은 Oracle DB catalog 기준의 TABLE/VIEW 목록으로 표시한다. +- VPD 화면에서는 VPD 적용 여부를 중심으로 보여주고, 백오피스 권한 테이블 등록 여부와 ORDS Path는 서빙 상태 참고 정보로 함께 표시한다. diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/config/DbPoolWarmup.java b/src/main/java/com/cloudhandson/vpdbackoffice/config/DbPoolWarmup.java index 0c7840b..85103fd 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/config/DbPoolWarmup.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/config/DbPoolWarmup.java @@ -2,6 +2,7 @@ package com.cloudhandson.vpdbackoffice.config; import com.cloudhandson.vpdbackoffice.service.PermissionService; import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService; +import com.cloudhandson.vpdbackoffice.service.VpdPolicyService; import java.sql.Connection; import javax.sql.DataSource; import org.slf4j.Logger; @@ -17,15 +18,18 @@ public class DbPoolWarmup { private final DataSource dataSource; private final ProtectedObjectService protectedObjectService; private final PermissionService permissionService; + private final VpdPolicyService vpdPolicyService; public DbPoolWarmup( DataSource dataSource, ProtectedObjectService protectedObjectService, - PermissionService permissionService + PermissionService permissionService, + VpdPolicyService vpdPolicyService ) { this.dataSource = dataSource; this.protectedObjectService = protectedObjectService; this.permissionService = permissionService; + this.vpdPolicyService = vpdPolicyService; } @EventListener(ApplicationReadyEvent.class) @@ -45,6 +49,8 @@ public class DbPoolWarmup { protectedObjectService.findDatabaseObjects(); permissionService.findRoles(); permissionService.findPermissionViews(); + vpdPolicyService.findVpdTargets(); + vpdPolicyService.formOptions(); log.info("Backoffice DB catalog cache warmed up in {}ms", (System.nanoTime() - started) / 1_000_000); } } diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdTargetView.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdTargetView.java new file mode 100644 index 0000000..4f855a6 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdTargetView.java @@ -0,0 +1,24 @@ +package com.cloudhandson.vpdbackoffice.domain.vpd; + +public record VpdTargetView( + String owner, + String objectName, + String objectType, + String protectedYn, + String ordsPath, + int policyCount, + String policyNames +) { + + public String objectDisplayName() { + return owner + "." + objectName; + } + + public boolean protectedObject() { + return "Y".equalsIgnoreCase(protectedYn); + } + + public boolean vpdApplied() { + return policyCount > 0; + } +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/VpdPolicyMapper.java b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/VpdPolicyMapper.java index e47e32a..83a9136 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/mapper/VpdPolicyMapper.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/mapper/VpdPolicyMapper.java @@ -3,6 +3,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 com.cloudhandson.vpdbackoffice.domain.vpd.VpdTargetView; import java.util.List; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; @@ -12,6 +13,8 @@ public interface VpdPolicyMapper { List findPolicies(); + List findVpdTargets(); + List findPolicyNameOptions(); List findSchemaOwnerOptions(); diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java index 9cd09b7..8611ae5 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java @@ -8,6 +8,7 @@ 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.domain.vpd.VpdTargetView; import com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper; import java.util.List; import java.util.Locale; @@ -21,10 +22,13 @@ import org.springframework.transaction.annotation.Transactional; public class VpdPolicyService { private static final Set ALLOWED_STATEMENTS = Set.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX"); + private static final long CATALOG_CACHE_MILLIS = 60_000L; private final VpdPolicyMapper mapper; private final JdbcTemplate jdbcTemplate; private final OpenAiCompatibleClient aiClient; + private volatile CacheEntry> vpdTargetsCache; + private volatile CacheEntry formOptionsCache; public VpdPolicyService(VpdPolicyMapper mapper, JdbcTemplate jdbcTemplate, OpenAiCompatibleClient aiClient) { this.mapper = mapper; @@ -36,14 +40,30 @@ public class VpdPolicyService { return mapper.findPolicies(); } + public List findVpdTargets() { + CacheEntry> cached = vpdTargetsCache; + if (cached != null && !cached.expired()) { + return cached.value(); + } + List targets = List.copyOf(mapper.findVpdTargets()); + vpdTargetsCache = new CacheEntry<>(targets, System.currentTimeMillis() + CATALOG_CACHE_MILLIS); + return targets; + } + public VpdPolicyFormOptions formOptions() { - return new VpdPolicyFormOptions( + CacheEntry cached = formOptionsCache; + if (cached != null && !cached.expired()) { + return cached.value(); + } + VpdPolicyFormOptions options = new VpdPolicyFormOptions( mapper.findPolicyNameOptions(), mapper.findSchemaOwnerOptions(), mapper.findOwnerOptions(), mapper.findFunctionOptions(), List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX") ); + formOptionsCache = new CacheEntry<>(options, System.currentTimeMillis() + CATALOG_CACHE_MILLIS); + return options; } public VpdPolicyFormOptions emptyFormOptions() { @@ -72,6 +92,7 @@ public class VpdPolicyService { throw new AppException("Filter predicate는 필수입니다."); } createFilterFunction(functionName, filterPredicate); + clearCatalogCache(); } @Transactional @@ -93,6 +114,7 @@ public class VpdPolicyService { END; """, objectOwner, objectName, policyName); createPolicy(command); + clearCatalogCache(); } public VpdBulkApplyResult bulkApplySchema( @@ -178,6 +200,7 @@ public class VpdPolicyService { failed++; } } + clearCatalogCache(); return new VpdBulkApplyResult(targets.size(), created, skipped, failed); } @@ -246,6 +269,12 @@ public class VpdPolicyService { command.enabled(), command.updateCheck() ); + clearCatalogCache(); + } + + public void clearCatalogCache() { + vpdTargetsCache = null; + formOptionsCache = null; } private void addPolicy( @@ -496,6 +525,13 @@ public class VpdPolicyService { return value.replace("'", "''"); } + private record CacheEntry(T value, long expiresAt) { + + boolean expired() { + return System.currentTimeMillis() > expiresAt; + } + } + private record FunctionRef(String owner, String packageName, String functionName) { } diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java index d111e64..1fb0b8a 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java @@ -39,12 +39,14 @@ public class VpdPolicyController { private void populatePolicyModel(Model model) { try { model.addAttribute("policies", vpdPolicyService.findPolicies()); + model.addAttribute("vpdTargets", vpdPolicyService.findVpdTargets()); model.addAttribute("objects", protectedObjectService.findEnabled()); model.addAttribute("formOptions", vpdPolicyService.formOptions()); } catch (DataAccessException exception) { RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception); model.addAttribute("runtimeError", message); model.addAttribute("policies", List.of()); + model.addAttribute("vpdTargets", List.of()); model.addAttribute("objects", List.of()); model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions()); } diff --git a/src/main/resources/mapper/VpdPolicyMapper.xml b/src/main/resources/mapper/VpdPolicyMapper.xml index 4d9073a..ccd9fd8 100644 --- a/src/main/resources/mapper/VpdPolicyMapper.xml +++ b/src/main/resources/mapper/VpdPolicyMapper.xml @@ -38,6 +38,45 @@ ORDER BY p.object_owner, p.object_name, p.policy_name + +