fix #424: use dropdowns for VPD policy form

This commit is contained in:
devmrko
2026-06-25 17:03:44 +09:00
parent 1bcb6b6a43
commit 9a546b7997
9 changed files with 190 additions and 15 deletions

View File

@@ -0,0 +1,18 @@
package com.cloudhandson.vpdbackoffice.domain.vpd;
public record VpdFunctionOption(
String owner,
String packageName,
String functionName,
String objectType
) {
public String value() {
String packagePrefix = packageName == null || packageName.isBlank() ? "" : packageName + ".";
return owner + "." + packagePrefix + functionName;
}
public String label() {
return value() + " / " + objectType;
}
}

View File

@@ -4,6 +4,7 @@ public record VpdPolicyCreateCommand(
String objectOwner, String objectOwner,
String objectName, String objectName,
String policyName, String policyName,
String functionKey,
String functionOwner, String functionOwner,
String functionName, String functionName,
String statementTypes, String statementTypes,

View File

@@ -0,0 +1,11 @@
package com.cloudhandson.vpdbackoffice.domain.vpd;
import java.util.List;
public record VpdPolicyFormOptions(
List<String> policyNames,
List<String> owners,
List<VpdFunctionOption> functions,
List<String> statementTypes
) {
}

View File

@@ -1,5 +1,6 @@
package com.cloudhandson.vpdbackoffice.mapper; package com.cloudhandson.vpdbackoffice.mapper;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionOption;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView;
import java.util.List; import java.util.List;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@@ -10,6 +11,12 @@ public interface VpdPolicyMapper {
List<VpdPolicyView> findPolicies(); List<VpdPolicyView> findPolicies();
List<String> findPolicyNameOptions();
List<String> findOwnerOptions();
List<VpdFunctionOption> findFunctionOptions();
VpdPolicyView findPolicy( VpdPolicyView findPolicy(
@Param("objectOwner") String objectOwner, @Param("objectOwner") String objectOwner,
@Param("objectName") String objectName, @Param("objectName") String objectName,

View File

@@ -4,6 +4,7 @@ import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionSource;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyCreateCommand; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyCreateCommand;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyDetail; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyDetail;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyExplanation; 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.VpdPolicyView;
import com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper; import com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper;
import java.util.List; import java.util.List;
@@ -32,6 +33,24 @@ public class VpdPolicyService {
return mapper.findPolicies(); return mapper.findPolicies();
} }
public VpdPolicyFormOptions formOptions() {
return new VpdPolicyFormOptions(
mapper.findPolicyNameOptions(),
mapper.findOwnerOptions(),
mapper.findFunctionOptions(),
List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX")
);
}
public VpdPolicyFormOptions emptyFormOptions() {
return new VpdPolicyFormOptions(
List.of(),
List.of(),
List.of(),
List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX")
);
}
public VpdFunctionSource findFunctionSource(String owner, String packageName, String functionName) { public VpdFunctionSource findFunctionSource(String owner, String packageName, String functionName) {
String normalizedOwner = requiredIdentifier(owner, "Function owner"); String normalizedOwner = requiredIdentifier(owner, "Function owner");
String normalizedFunction = requiredIdentifier(functionName, "Function name"); String normalizedFunction = requiredIdentifier(functionName, "Function name");
@@ -59,12 +78,23 @@ public class VpdPolicyService {
String objectName = requiredIdentifier(command.objectName(), "Object name"); String objectName = requiredIdentifier(command.objectName(), "Object name");
String policyName = requiredIdentifier(command.policyName(), "Policy name"); String policyName = requiredIdentifier(command.policyName(), "Policy name");
String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class); String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class);
String functionOwner = command.functionOwner() == null || command.functionOwner().isBlank() FunctionRef functionRef = parseFunctionRef(command.functionKey());
? currentUser String functionOwner;
: requiredIdentifier(command.functionOwner(), "Function owner"); String packageName;
String functionName = command.functionName() == null || command.functionName().isBlank() String functionName;
? generatedFunctionName(policyName) if (functionRef != null) {
: requiredIdentifier(command.functionName(), "Function name"); functionOwner = functionRef.owner();
packageName = functionRef.packageName();
functionName = functionRef.functionName();
} else {
functionOwner = command.functionOwner() == null || command.functionOwner().isBlank()
? currentUser
: requiredIdentifier(command.functionOwner(), "Function owner");
packageName = null;
functionName = command.functionName() == null || command.functionName().isBlank()
? generatedFunctionName(policyName)
: requiredIdentifier(command.functionName(), "Function name");
}
String statementTypes = normalizeStatementTypes(command.statementTypes()); String statementTypes = normalizeStatementTypes(command.statementTypes());
String filterPredicate = command.filterPredicate() == null ? "" : command.filterPredicate().trim(); String filterPredicate = command.filterPredicate() == null ? "" : command.filterPredicate().trim();
@@ -95,7 +125,7 @@ public class VpdPolicyService {
objectName, objectName,
policyName, policyName,
functionOwner, functionOwner,
functionName, packageName == null ? functionName : packageName + "." + functionName,
statementTypes); statementTypes);
} }
@@ -271,6 +301,22 @@ public class VpdPolicyService {
return generated.length() > 128 ? generated.substring(0, 128) : generated; return generated.length() > 128 ? generated.substring(0, 128) : generated;
} }
private FunctionRef parseFunctionRef(String functionKey) {
if (functionKey == null || functionKey.isBlank()) {
return null;
}
String[] parts = functionKey.trim().toUpperCase(Locale.ROOT).split("\\.");
if (parts.length == 2) {
return new FunctionRef(requiredIdentifier(parts[0], "Function owner"), null,
requiredIdentifier(parts[1], "Function name"));
}
if (parts.length == 3) {
return new FunctionRef(requiredIdentifier(parts[0], "Function owner"),
requiredIdentifier(parts[1], "Package name"), requiredIdentifier(parts[2], "Function name"));
}
throw new AppException("Function 선택 값이 올바르지 않습니다: " + functionKey);
}
private String normalizeStatementTypes(String value) { private String normalizeStatementTypes(String value) {
String raw = value == null || value.isBlank() ? "SELECT" : value; String raw = value == null || value.isBlank() ? "SELECT" : value;
List<String> statements = List.of(raw.split(",")).stream() List<String> statements = List.of(raw.split(",")).stream()
@@ -293,6 +339,9 @@ public class VpdPolicyService {
return value.replace("'", "''"); return value.replace("'", "''");
} }
private record FunctionRef(String owner, String packageName, String functionName) {
}
private String policyFunctionArgument(VpdPolicyView policy) { private String policyFunctionArgument(VpdPolicyView policy) {
if (policy.packageName() == null || policy.packageName().isBlank()) { if (policy.packageName() == null || policy.packageName().isBlank()) {
return policy.functionName(); return policy.functionName();

View File

@@ -29,11 +29,13 @@ public class VpdPolicyController {
try { try {
model.addAttribute("policies", vpdPolicyService.findPolicies()); model.addAttribute("policies", vpdPolicyService.findPolicies());
model.addAttribute("objects", protectedObjectService.findEnabled()); model.addAttribute("objects", protectedObjectService.findEnabled());
model.addAttribute("formOptions", vpdPolicyService.formOptions());
} catch (DataAccessException exception) { } catch (DataAccessException exception) {
RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception); RuntimeErrorMessage message = RuntimeErrorMessages.dataAccess(exception);
model.addAttribute("runtimeError", message); model.addAttribute("runtimeError", message);
model.addAttribute("policies", List.of()); model.addAttribute("policies", List.of());
model.addAttribute("objects", List.of()); model.addAttribute("objects", List.of());
model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions());
} }
return "vpd-policies"; return "vpd-policies";
} }
@@ -42,9 +44,10 @@ public class VpdPolicyController {
public String createPolicy( public String createPolicy(
@RequestParam String objectKey, @RequestParam String objectKey,
@RequestParam String policyName, @RequestParam String policyName,
@RequestParam(required = false) String functionKey,
@RequestParam(required = false) String functionOwner, @RequestParam(required = false) String functionOwner,
@RequestParam(required = false) String functionName, @RequestParam(required = false) String functionName,
@RequestParam(defaultValue = "SELECT") String statementTypes, @RequestParam(defaultValue = "SELECT") List<String> statementTypes,
@RequestParam(defaultValue = "false") boolean enabled, @RequestParam(defaultValue = "false") boolean enabled,
@RequestParam(defaultValue = "false") boolean updateCheck, @RequestParam(defaultValue = "false") boolean updateCheck,
@RequestParam(required = false) String filterPredicate, @RequestParam(required = false) String filterPredicate,
@@ -59,9 +62,10 @@ public class VpdPolicyController {
objectParts[0], objectParts[0],
objectParts[1], objectParts[1],
policyName, policyName,
functionKey,
functionOwner, functionOwner,
functionName, functionName,
statementTypes, String.join(",", statementTypes),
enabled, enabled,
updateCheck, updateCheck,
filterPredicate filterPredicate

View File

@@ -38,6 +38,47 @@
ORDER BY p.object_owner, p.object_name, p.policy_name ORDER BY p.object_owner, p.object_name, p.policy_name
</select> </select>
<select id="findPolicyNameOptions" resultType="string">
SELECT DISTINCT policy_name
FROM all_policies
WHERE policy_name IS NOT NULL
UNION
SELECT object_name || '_POLICY'
FROM cb_protected_object
WHERE enabled_yn = 'Y'
ORDER BY 1
</select>
<select id="findOwnerOptions" resultType="string">
SELECT USER FROM dual
UNION
SELECT owner FROM cb_protected_object WHERE enabled_yn = 'Y'
UNION
SELECT pf_owner FROM all_policies WHERE pf_owner IS NOT NULL
ORDER BY 1
</select>
<select id="findFunctionOptions" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionOption">
WITH allowed_owners AS (
SELECT USER AS owner FROM dual
UNION
SELECT owner FROM cb_protected_object WHERE enabled_yn = 'Y'
UNION
SELECT pf_owner FROM all_policies WHERE pf_owner IS NOT NULL
)
SELECT owner,
package_name,
object_name AS function_name,
CASE WHEN package_name IS NULL THEN 'FUNCTION' ELSE 'PACKAGE FUNCTION' END AS object_type
FROM all_arguments
WHERE owner IN (SELECT owner FROM allowed_owners)
AND position = 0
AND data_type = 'VARCHAR2'
AND object_name IS NOT NULL
ORDER BY owner, package_name, function_name
FETCH FIRST 300 ROWS ONLY
</select>
<select id="findPolicy" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView"> <select id="findPolicy" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView">
SELECT <include refid="policyColumns"><property name="alias" value="p."/></include> SELECT <include refid="policyColumns"><property name="alias" value="p."/></include>
FROM all_policies p FROM all_policies p

View File

@@ -348,6 +348,18 @@ body {
text-align: left; text-align: left;
} }
.checkbox-row {
display: flex;
flex-wrap: wrap;
gap: .75rem 1rem;
min-height: 2.45rem;
align-items: center;
}
.checkbox-row .form-check {
margin: 0;
}
.rw-btn, .rw-btn,
.btn { .btn {
border-radius: 999px; border-radius: 999px;

View File

@@ -32,20 +32,52 @@
</label> </label>
<label> <label>
Policy 이름 Policy 이름
<input class="form-control" name="policyName" placeholder="예: BOARD_POSTS_POLICY" required> <select class="form-select" name="policyName" required>
<option th:each="policyName : ${formOptions.policyNames()}"
th:value="${policyName}"
th:text="${policyName}"></option>
</select>
</label>
<label class="span-2">
기존 Function 선택
<select class="form-select" name="functionKey">
<option value="">Filter predicate로 새 function 자동 생성</option>
<option th:each="function : ${formOptions.functions()}"
th:value="${function.value()}"
th:text="${function.label()}"></option>
</select>
</label> </label>
<label> <label>
Function Owner Function Owner
<input class="form-control" name="functionOwner" placeholder="비우면 현재 연결 사용자"> <select class="form-select" name="functionOwner">
<option value="">현재 연결 사용자</option>
<option th:each="owner : ${formOptions.owners()}"
th:value="${owner}"
th:text="${owner}"></option>
</select>
</label> </label>
<label> <label>
Function 이름 Function 이름
<input class="form-control" name="functionName" placeholder="비우면 POLICY_NAME_FILTER 자동 생성"> <select class="form-select" name="functionName">
<option value="">POLICY_NAME_FILTER 자동 생성</option>
<option th:each="function : ${formOptions.functions()}"
th:value="${function.functionName()}"
th:text="${function.functionName()}"></option>
</select>
</label> </label>
<label> <div>
Statement Types Statement Types
<input class="form-control" name="statementTypes" value="SELECT" placeholder="SELECT,INSERT,UPDATE,DELETE"> <div class="checkbox-row">
</label> <label class="form-check" th:each="statement : ${formOptions.statementTypes()}">
<input class="form-check-input"
type="checkbox"
name="statementTypes"
th:value="${statement}"
th:checked="${statement == 'SELECT'}">
<span class="form-check-label" th:text="${statement}">SELECT</span>
</label>
</div>
</div>
<div class="form-check align-self-end"> <div class="form-check align-self-end">
<input class="form-check-input" id="vpd-enabled" type="checkbox" name="enabled" value="true" checked> <input class="form-check-input" id="vpd-enabled" type="checkbox" name="enabled" value="true" checked>
<label class="form-check-label" for="vpd-enabled">등록 즉시 활성화</label> <label class="form-check-label" for="vpd-enabled">등록 즉시 활성화</label>