diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdPolicyCreateCommand.java b/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdPolicyCreateCommand.java new file mode 100644 index 0000000..b276011 --- /dev/null +++ b/src/main/java/com/cloudhandson/vpdbackoffice/domain/vpd/VpdPolicyCreateCommand.java @@ -0,0 +1,14 @@ +package com.cloudhandson.vpdbackoffice.domain.vpd; + +public record VpdPolicyCreateCommand( + String objectOwner, + String objectName, + String policyName, + String functionOwner, + String functionName, + String statementTypes, + boolean enabled, + boolean updateCheck, + String filterPredicate +) { +} diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java b/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java index 7170363..5ecc83e 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/service/VpdPolicyService.java @@ -1,22 +1,30 @@ package com.cloudhandson.vpdbackoffice.service; 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.VpdPolicyView; import com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper; import java.util.List; import java.util.Locale; +import java.util.Set; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service public class VpdPolicyService { + private static final Set ALLOWED_STATEMENTS = Set.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX"); + private final VpdPolicyMapper mapper; + private final JdbcTemplate jdbcTemplate; private final OpenAiCompatibleClient aiClient; - public VpdPolicyService(VpdPolicyMapper mapper, OpenAiCompatibleClient aiClient) { + public VpdPolicyService(VpdPolicyMapper mapper, JdbcTemplate jdbcTemplate, OpenAiCompatibleClient aiClient) { this.mapper = mapper; + this.jdbcTemplate = jdbcTemplate; this.aiClient = aiClient; } @@ -45,6 +53,52 @@ public class VpdPolicyService { return new VpdPolicyDetail(policy, buildAddPolicyBlock(policy)); } + @Transactional + public void createPolicy(VpdPolicyCreateCommand command) { + String objectOwner = requiredIdentifier(command.objectOwner(), "Object owner"); + String objectName = requiredIdentifier(command.objectName(), "Object name"); + String policyName = requiredIdentifier(command.policyName(), "Policy name"); + String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class); + String functionOwner = command.functionOwner() == null || command.functionOwner().isBlank() + ? currentUser + : requiredIdentifier(command.functionOwner(), "Function owner"); + String functionName = command.functionName() == null || command.functionName().isBlank() + ? generatedFunctionName(policyName) + : requiredIdentifier(command.functionName(), "Function name"); + String statementTypes = normalizeStatementTypes(command.statementTypes()); + String filterPredicate = command.filterPredicate() == null ? "" : command.filterPredicate().trim(); + + if (!filterPredicate.isBlank()) { + if (currentUser == null || !currentUser.equalsIgnoreCase(functionOwner)) { + throw new AppException("필터 함수 자동 생성은 현재 연결 사용자 스키마에만 가능합니다. 현재 사용자: " + + currentUser + ", Function owner: " + functionOwner); + } + createFilterFunction(functionName, filterPredicate); + } + + jdbcTemplate.update(""" + BEGIN + DBMS_RLS.ADD_POLICY( + object_schema => ?, + object_name => ?, + policy_name => ?, + function_schema => ?, + policy_function => ?, + statement_types => ?, + update_check => %s, + enable => %s, + policy_type => DBMS_RLS.DYNAMIC + ); + END; + """.formatted(command.updateCheck() ? "TRUE" : "FALSE", command.enabled() ? "TRUE" : "FALSE"), + objectOwner, + objectName, + policyName, + functionOwner, + functionName, + statementTypes); + } + public VpdPolicyExplanation explainPolicy(String objectOwner, String objectName, String policyName) { VpdPolicyDetail detail = findPolicyDetail(objectOwner, objectName, policyName); VpdPolicyView policy = detail.policy(); @@ -196,6 +250,49 @@ public class VpdPolicyService { ); } + private void createFilterFunction(String functionName, String filterPredicate) { + jdbcTemplate.execute(""" + CREATE OR REPLACE FUNCTION %s( + p_schema_name IN VARCHAR2, + p_object_name IN VARCHAR2 + ) RETURN VARCHAR2 + AS + BEGIN + RETURN '%s'; + END; + """.formatted(functionName, escapeSqlLiteral(filterPredicate))); + } + + private String generatedFunctionName(String policyName) { + String base = policyName.endsWith("_POLICY") + ? policyName.substring(0, policyName.length() - "_POLICY".length()) + : policyName; + String generated = base + "_FILTER"; + return generated.length() > 128 ? generated.substring(0, 128) : generated; + } + + private String normalizeStatementTypes(String value) { + String raw = value == null || value.isBlank() ? "SELECT" : value; + List statements = List.of(raw.split(",")).stream() + .map(String::trim) + .filter(token -> !token.isBlank()) + .map(token -> token.toUpperCase(Locale.ROOT)) + .toList(); + if (statements.isEmpty()) { + return "SELECT"; + } + for (String statement : statements) { + if (!ALLOWED_STATEMENTS.contains(statement)) { + throw new AppException("지원하지 않는 statement type입니다: " + statement); + } + } + return String.join(",", statements); + } + + private String escapeSqlLiteral(String value) { + return value.replace("'", "''"); + } + private String policyFunctionArgument(VpdPolicyView policy) { if (policy.packageName() == null || policy.packageName().isBlank()) { return policy.functionName(); diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java b/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java index 20dfe07..339bcc5 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/web/VpdPolicyController.java @@ -1,35 +1,81 @@ package com.cloudhandson.vpdbackoffice.web; +import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyCreateCommand; import com.cloudhandson.vpdbackoffice.service.AppException; +import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService; import com.cloudhandson.vpdbackoffice.service.VpdPolicyService; import java.util.List; import org.springframework.dao.DataAccessException; 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 VpdPolicyController { private final VpdPolicyService vpdPolicyService; + private final ProtectedObjectService protectedObjectService; - public VpdPolicyController(VpdPolicyService vpdPolicyService) { + public VpdPolicyController(VpdPolicyService vpdPolicyService, ProtectedObjectService protectedObjectService) { this.vpdPolicyService = vpdPolicyService; + this.protectedObjectService = protectedObjectService; } @GetMapping("/vpd-policies") public String policies(Model model) { try { model.addAttribute("policies", vpdPolicyService.findPolicies()); + model.addAttribute("objects", protectedObjectService.findEnabled()); } catch (DataAccessException exception) { RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception); model.addAttribute("runtimeError", message); model.addAttribute("policies", List.of()); + model.addAttribute("objects", List.of()); } return "vpd-policies"; } + @PostMapping("/vpd-policies") + public String createPolicy( + @RequestParam String objectKey, + @RequestParam String policyName, + @RequestParam(required = false) String functionOwner, + @RequestParam(required = false) String functionName, + @RequestParam(defaultValue = "SELECT") String statementTypes, + @RequestParam(defaultValue = "false") boolean enabled, + @RequestParam(defaultValue = "false") boolean updateCheck, + @RequestParam(required = false) String filterPredicate, + RedirectAttributes redirectAttributes + ) { + try { + String[] objectParts = objectKey.split("\\.", 2); + if (objectParts.length != 2) { + throw new AppException("조회 대상 형식이 올바르지 않습니다: " + objectKey); + } + vpdPolicyService.createPolicy(new VpdPolicyCreateCommand( + objectParts[0], + objectParts[1], + policyName, + functionOwner, + functionName, + statementTypes, + enabled, + updateCheck, + filterPredicate + )); + redirectAttributes.addFlashAttribute("successMessage", "VPD policy를 등록했습니다: " + policyName); + } 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/templates/vpd-policies.html b/src/main/resources/templates/vpd-policies.html index 2a52de9..232f7d7 100644 --- a/src/main/resources/templates/vpd-policies.html +++ b/src/main/resources/templates/vpd-policies.html @@ -6,13 +6,84 @@

VPD 설정

-

ORDS 조회 Handler 대상에 등록된 보호 객체에 적용된 Oracle VPD policy 설정을 확인합니다.

+

보호 객체에 Oracle VPD policy를 등록하고, policy function/filter predicate를 확인합니다.

조회할 수 없습니다. message
+
등록되었습니다.
+
처리할 수 없습니다.
+ +
+
+

Policy 등록

+
+
+ + + + + + +
+ + +
+
+ + +
+ +
+ + + + +
+ +
+
@@ -106,5 +177,17 @@
+