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 objectName,
String policyName,
String functionKey,
String functionOwner,
String functionName,
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;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionOption;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
@@ -10,6 +11,12 @@ public interface VpdPolicyMapper {
List<VpdPolicyView> findPolicies();
List<String> findPolicyNameOptions();
List<String> findOwnerOptions();
List<VpdFunctionOption> findFunctionOptions();
VpdPolicyView findPolicy(
@Param("objectOwner") String objectOwner,
@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.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.mapper.VpdPolicyMapper;
import java.util.List;
@@ -32,6 +33,24 @@ public class VpdPolicyService {
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) {
String normalizedOwner = requiredIdentifier(owner, "Function owner");
String normalizedFunction = requiredIdentifier(functionName, "Function name");
@@ -59,12 +78,23 @@ public class VpdPolicyService {
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");
FunctionRef functionRef = parseFunctionRef(command.functionKey());
String functionOwner;
String packageName;
String functionName;
if (functionRef != null) {
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 filterPredicate = command.filterPredicate() == null ? "" : command.filterPredicate().trim();
@@ -95,7 +125,7 @@ public class VpdPolicyService {
objectName,
policyName,
functionOwner,
functionName,
packageName == null ? functionName : packageName + "." + functionName,
statementTypes);
}
@@ -271,6 +301,22 @@ public class VpdPolicyService {
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) {
String raw = value == null || value.isBlank() ? "SELECT" : value;
List<String> statements = List.of(raw.split(",")).stream()
@@ -293,6 +339,9 @@ public class VpdPolicyService {
return value.replace("'", "''");
}
private record FunctionRef(String owner, String packageName, String functionName) {
}
private String policyFunctionArgument(VpdPolicyView policy) {
if (policy.packageName() == null || policy.packageName().isBlank()) {
return policy.functionName();

View File

@@ -29,11 +29,13 @@ public class VpdPolicyController {
try {
model.addAttribute("policies", vpdPolicyService.findPolicies());
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("objects", List.of());
model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions());
}
return "vpd-policies";
}
@@ -42,9 +44,10 @@ public class VpdPolicyController {
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") String statementTypes,
@RequestParam(defaultValue = "SELECT") List<String> statementTypes,
@RequestParam(defaultValue = "false") boolean enabled,
@RequestParam(defaultValue = "false") boolean updateCheck,
@RequestParam(required = false) String filterPredicate,
@@ -59,9 +62,10 @@ public class VpdPolicyController {
objectParts[0],
objectParts[1],
policyName,
functionKey,
functionOwner,
functionName,
statementTypes,
String.join(",", statementTypes),
enabled,
updateCheck,
filterPredicate

View File

@@ -38,6 +38,47 @@
ORDER BY p.object_owner, p.object_name, p.policy_name
</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 <include refid="policyColumns"><property name="alias" value="p."/></include>
FROM all_policies p

View File

@@ -348,6 +348,18 @@ body {
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,
.btn {
border-radius: 999px;

View File

@@ -32,20 +32,52 @@
</label>
<label>
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>
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>
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>
<div>
Statement Types
<input class="form-control" name="statementTypes" value="SELECT" placeholder="SELECT,INSERT,UPDATE,DELETE">
</label>
<div class="checkbox-row">
<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">
<input class="form-check-input" id="vpd-enabled" type="checkbox" name="enabled" value="true" checked>
<label class="form-check-label" for="vpd-enabled">등록 즉시 활성화</label>