package com.cloudhandson.vpdbackoffice.web; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyCreateCommand; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView; import com.cloudhandson.vpdbackoffice.service.AppException; import com.cloudhandson.vpdbackoffice.service.VpdPolicyService; import java.util.List; import java.util.LinkedHashMap; import java.util.Map; import org.springframework.dao.DataAccessException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 static final Logger log = LoggerFactory.getLogger(VpdPolicyController.class); private final VpdPolicyService vpdPolicyService; public VpdPolicyController(VpdPolicyService vpdPolicyService) { this.vpdPolicyService = vpdPolicyService; } @GetMapping("/vpd-policies") public String policies( @RequestParam(required = false) String schemaOwner, Model model ) { populatePolicyModel(schemaOwner, false, model); return "vpd-policies"; } @GetMapping("/vpd-filter-policies") public String filterPolicies( @RequestParam(required = false) String schemaOwner, Model model ) { populatePolicyModel(schemaOwner, true, model); return "vpd-filter-policies"; } /** Read-only operational view of the default dynamic permission filter. */ @GetMapping("/vpd-filter-runtime") public String filterRuntime(Model model) { try { List policies = vpdPolicyService.findPolicies().stream() .filter(VpdPolicyView::permissionSystemDefault) .toList(); model.addAttribute("policies", policies); if (!policies.isEmpty()) { VpdPolicyView filter = policies.get(0); model.addAttribute("source", vpdPolicyService.findFunctionSource( filter.functionOwner(), filter.packageName(), filter.functionName())); } } catch (DataAccessException exception) { RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception); model.addAttribute("runtimeError", message); model.addAttribute("policies", List.of()); } catch (AppException exception) { model.addAttribute("errorMessage", exception.getMessage()); model.addAttribute("policies", List.of()); } return "vpd-filter-runtime"; } private void populatePolicyModel(String schemaOwner, boolean includeFilterEditor, Model model) { try { long started = System.nanoTime(); String selectedSchemaOwner = schemaOwner == null ? "" : schemaOwner.trim().toUpperCase(); List policies = vpdPolicyService.findPolicies(); long policiesAt = System.nanoTime(); Map policyDescriptions = new LinkedHashMap<>(); if (!includeFilterEditor) { policyDescriptions.putAll(vpdPolicyService.findPolicyDescriptionMap()); policies.forEach(policy -> policyDescriptions.put( policy.objectDisplayName() + "|" + policy.policyName(), policyDescriptions.getOrDefault( policy.objectDisplayName() + "|" + policy.policyName(), policy.objectDisplayName() + "에 요청마다 현재 권한체계의 행 접근 조건을 적용하는 " + policy.policyName() + " policy입니다." ) )); } long policyDescriptionsAt = System.nanoTime(); model.addAttribute("policies", policies); model.addAttribute("policyDescriptions", policyDescriptions); model.addAttribute("vpdTargets", includeFilterEditor ? List.of() : vpdPolicyService.findVpdTargets(selectedSchemaOwner)); long targetsAt = System.nanoTime(); model.addAttribute("selectedSchemaOwner", selectedSchemaOwner); var formOptions = vpdPolicyService.formOptions(); long formOptionsAt = System.nanoTime(); Map filterDescriptions = new LinkedHashMap<>(); filterDescriptions.putAll(vpdPolicyService.findFilterDescriptionMap()); policies.forEach(policy -> filterDescriptions.putIfAbsent( policy.functionOwner() + "|" + policy.functionName(), defaultFilterDescription(policy.functionName()) )); if (includeFilterEditor) { formOptions.functions().forEach(function -> filterDescriptions.putIfAbsent( function.owner() + "|" + function.functionName(), defaultFilterDescription(function.functionName()) )); } long filterDescriptionsAt = System.nanoTime(); Map filterPredicates = new LinkedHashMap<>(); if (includeFilterEditor) { formOptions.functions().forEach(function -> filterPredicates.put( function.owner() + "|" + function.functionName(), vpdPolicyService.findFilterPredicate(function.owner(), function.packageName(), function.functionName()) )); } long predicatesAt = System.nanoTime(); model.addAttribute("formOptions", formOptions); model.addAttribute("filterDescriptions", filterDescriptions); model.addAttribute("filterPredicates", filterPredicates); log.info("vpd page timings: editor={} policies={}ms policyDescriptions={}ms targets={}ms formOptions={}ms filterDescriptions={}ms predicates={}ms total={}ms", includeFilterEditor, elapsedMillis(started, policiesAt), elapsedMillis(policiesAt, policyDescriptionsAt), elapsedMillis(policyDescriptionsAt, targetsAt), elapsedMillis(targetsAt, formOptionsAt), elapsedMillis(formOptionsAt, filterDescriptionsAt), elapsedMillis(filterDescriptionsAt, predicatesAt), elapsedMillis(started, predicatesAt)); } catch (DataAccessException exception) { RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception); model.addAttribute("runtimeError", message); model.addAttribute("policies", List.of()); model.addAttribute("vpdTargets", List.of()); model.addAttribute("selectedSchemaOwner", ""); model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions()); model.addAttribute("policyDescriptions", Map.of()); model.addAttribute("filterDescriptions", Map.of()); model.addAttribute("filterPredicates", Map.of()); } catch (AppException exception) { model.addAttribute("errorMessage", exception.getMessage()); model.addAttribute("policies", List.of()); model.addAttribute("vpdTargets", List.of()); model.addAttribute("selectedSchemaOwner", ""); model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions()); model.addAttribute("policyDescriptions", Map.of()); model.addAttribute("filterDescriptions", Map.of()); model.addAttribute("filterPredicates", Map.of()); } } private static String defaultFilterDescription(String functionName) { return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName) ? "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다." : "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다."; } private static long elapsedMillis(long started, long finished) { return (finished - started) / 1_000_000; } @PostMapping("/vpd-policies") public String createPolicy( @RequestParam String objectKey, @RequestParam String policyName, @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 ) { createPolicyInternal(objectKey, policyName, functionKey, functionOwner, functionName, statementTypes, enabled, updateCheck, filterPredicate, redirectAttributes); return "redirect:/vpd-policies"; } @PostMapping("/vpd-policies/default") public String createDefaultPolicy( @RequestParam String objectKey, RedirectAttributes redirectAttributes ) { try { vpdPolicyService.createDefaultPermissionPolicy(objectKey); redirectAttributes.addFlashAttribute( "successMessage", "권한체계 자동 필터를 연결했습니다: " + objectKey ); } 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"; } @PostMapping("/vpd-filter-policies/filters") public String saveFilter( @RequestParam(required = false) String functionOwner, @RequestParam String functionName, @RequestParam String filterPredicate, @RequestParam(defaultValue = "Filter function이 반환하는 predicate로 조회 행을 제한합니다.") String description, RedirectAttributes redirectAttributes ) { try { vpdPolicyService.saveFilterFunction(functionOwner, functionName, filterPredicate); vpdPolicyService.saveFilterDescription( functionOwner == null || functionOwner.isBlank() ? vpdPolicyService.currentUser() : functionOwner, functionName, description ); redirectAttributes.addFlashAttribute("successMessage", "Filter function을 저장했습니다: " + functionName); } catch (AppException exception) { redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage()); } catch (DataAccessException exception) { RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception); redirectAttributes.addFlashAttribute("errorMessage", message.message()); } return "redirect:/vpd-filter-policies"; } @PostMapping("/vpd-policies/description") public String savePolicyDescription( @RequestParam String objectOwner, @RequestParam String objectName, @RequestParam String policyName, @RequestParam String description, RedirectAttributes redirectAttributes ) { try { vpdPolicyService.savePolicyDescription(objectOwner, objectName, policyName, description); redirectAttributes.addFlashAttribute("successMessage", "Policy 설명을 저장했습니다."); } catch (AppException exception) { redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage()); } return "redirect:/vpd-policies"; } @PostMapping("/vpd-filter-policies/replace") public String replaceFilterPolicy( @RequestParam String oldObjectKey, @RequestParam String oldPolicyName, @RequestParam String objectKey, @RequestParam String policyName, @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 { String[] objectParts = objectKey.split("\\.", 2); if (objectParts.length != 2) { throw new AppException("조회 대상 형식이 올바르지 않습니다: " + objectKey); } vpdPolicyService.replacePolicy(oldObjectKey, oldPolicyName, new VpdPolicyCreateCommand( objectParts[0], objectParts[1], policyName, functionKey, functionOwner, functionName, String.join(",", statementTypes), enabled, updateCheck, filterPredicate )); redirectAttributes.addFlashAttribute("successMessage", "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-filter-policies"; } private void createPolicyInternal( String objectKey, String policyName, String functionKey, String functionOwner, String functionName, List statementTypes, boolean enabled, boolean updateCheck, 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, functionKey, functionOwner, functionName, String.join(",", 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()); } } @PostMapping("/vpd-policies/bulk") public String bulkApplyPolicy( @RequestParam String schemaOwner, @RequestParam(defaultValue = "false") boolean includeTables, @RequestParam(defaultValue = "false") boolean includeViews, @RequestParam(required = false) String policyName, @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 ) { bulkApplyPolicyInternal(schemaOwner, includeTables, includeViews, policyName, functionKey, functionOwner, functionName, statementTypes, enabled, updateCheck, filterPredicate, redirectAttributes); return "redirect:/vpd-policies"; } private void bulkApplyPolicyInternal( String schemaOwner, boolean includeTables, boolean includeViews, String policyName, String functionKey, String functionOwner, String functionName, List statementTypes, boolean enabled, boolean updateCheck, String filterPredicate, RedirectAttributes redirectAttributes ) { try { var result = vpdPolicyService.bulkApplySchema( schemaOwner, includeTables, includeViews, policyName, 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()); } } @GetMapping("/vpd-policies/function-source") public String functionSource( @RequestParam String owner, @RequestParam(required = false) String packageName, @RequestParam String functionName, Model model ) { try { model.addAttribute("source", vpdPolicyService.findFunctionSource(owner, packageName, functionName)); } catch (AppException exception) { model.addAttribute("errorMessage", exception.getMessage()); } catch (DataAccessException exception) { RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception); model.addAttribute("errorMessage", message.message()); } return "fragments/vpd-function-source :: source"; } @GetMapping("/vpd-policies/policy-detail") public String policyDetail( @RequestParam String objectOwner, @RequestParam String objectName, @RequestParam String policyName, Model model ) { try { model.addAttribute("detail", vpdPolicyService.findPolicyDetail(objectOwner, objectName, policyName)); } catch (AppException exception) { model.addAttribute("errorMessage", exception.getMessage()); } catch (DataAccessException exception) { RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception); model.addAttribute("errorMessage", message.message()); } return "fragments/vpd-policy-detail :: detail"; } @GetMapping("/vpd-policies/object-filter-detail") public String objectFilterDetail( @RequestParam String objectOwner, @RequestParam String objectName, Model model ) { try { model.addAttribute("detail", vpdPolicyService.findObjectFilterDetail(objectOwner, objectName)); } catch (AppException exception) { model.addAttribute("errorMessage", exception.getMessage()); } catch (DataAccessException exception) { RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception); model.addAttribute("errorMessage", message.message()); } return "fragments/vpd-object-filter-detail :: detail"; } @GetMapping("/vpd-policies/policy-explanation") public String policyExplanation( @RequestParam String objectOwner, @RequestParam String objectName, @RequestParam String policyName, Model model ) { try { model.addAttribute("explanation", vpdPolicyService.explainPolicy(objectOwner, objectName, policyName)); } catch (AppException exception) { model.addAttribute("errorMessage", exception.getMessage()); } catch (DataAccessException exception) { RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception); model.addAttribute("errorMessage", message.message()); } return "fragments/vpd-policy-explanation :: explanation"; } }