[Developer] #424 show VPD policy details

This commit is contained in:
devmrko
2026-06-25 11:18:22 +09:00
parent 86e2b252c9
commit 1c3894cb40
7 changed files with 167 additions and 23 deletions

View File

@@ -0,0 +1,7 @@
package com.cloudhandson.vpdbackoffice.domain.vpd;
public record VpdPolicyDetail(
VpdPolicyView policy,
String ddl
) {
}

View File

@@ -10,6 +10,12 @@ public interface VpdPolicyMapper {
List<VpdPolicyView> findPolicies(); List<VpdPolicyView> findPolicies();
VpdPolicyView findPolicy(
@Param("objectOwner") String objectOwner,
@Param("objectName") String objectName,
@Param("policyName") String policyName
);
String findFunctionSource( String findFunctionSource(
@Param("owner") String owner, @Param("owner") String owner,
@Param("objectName") String objectName, @Param("objectName") String objectName,

View File

@@ -1,6 +1,7 @@
package com.cloudhandson.vpdbackoffice.service; package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionSource; import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionSource;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyDetail;
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;
@@ -30,6 +31,72 @@ public class VpdPolicyService {
return new VpdFunctionSource(normalizedOwner, objectName, objectType, source); return new VpdFunctionSource(normalizedOwner, objectName, objectType, source);
} }
public VpdPolicyDetail findPolicyDetail(String objectOwner, String objectName, String policyName) {
String normalizedOwner = requiredIdentifier(objectOwner, "Object owner");
String normalizedObject = requiredIdentifier(objectName, "Object name");
String normalizedPolicy = requiredIdentifier(policyName, "Policy name");
VpdPolicyView policy = mapper.findPolicy(normalizedOwner, normalizedObject, normalizedPolicy);
if (policy == null) {
throw new AppException("VPD policy를 찾을 수 없습니다.");
}
return new VpdPolicyDetail(policy, buildAddPolicyBlock(policy));
}
private String buildAddPolicyBlock(VpdPolicyView policy) {
return """
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => '%s',
object_name => '%s',
policy_name => '%s',
function_schema => '%s',
policy_function => '%s',
statement_types => '%s',
update_check => %s,
enable => %s,
static_policy => %s,
policy_type => %s,
long_predicate => %s
);
END;
/
""".formatted(
policy.objectOwner(),
policy.objectName(),
policy.policyName(),
policy.functionOwner(),
policyFunctionArgument(policy),
blankToDefault(policy.statementTypes(), "SELECT"),
yesNoBoolean(policy.checkOption()),
yesNoBoolean(policy.enabled()),
yesNoBoolean(policy.staticPolicy()),
policyTypeArgument(policy.policyType()),
yesNoBoolean(policy.longPredicate())
);
}
private String policyFunctionArgument(VpdPolicyView policy) {
if (policy.packageName() == null || policy.packageName().isBlank()) {
return policy.functionName();
}
return policy.packageName() + "." + policy.functionName();
}
private String blankToDefault(String value, String defaultValue) {
return value == null || value.isBlank() ? defaultValue : value;
}
private String yesNoBoolean(String value) {
return "YES".equalsIgnoreCase(value) ? "TRUE" : "FALSE";
}
private String policyTypeArgument(String policyType) {
if (policyType == null || policyType.isBlank()) {
return "NULL";
}
return "DBMS_RLS." + policyType.trim().toUpperCase(Locale.ROOT);
}
private String requiredIdentifier(String value, String label) { private String requiredIdentifier(String value, String label) {
String normalized = normalizeOptionalIdentifier(value); String normalized = normalizeOptionalIdentifier(value);
if (normalized == null) { if (normalized == null) {

View File

@@ -47,4 +47,22 @@ public class VpdPolicyController {
} }
return "fragments/vpd-function-source :: source"; 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";
}
} }

View File

@@ -2,8 +2,7 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd"> "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper"> <mapper namespace="com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper">
<select id="findPolicies" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView"> <sql id="policyColumns">
SELECT
object_owner, object_owner,
object_name, object_name,
policy_group, policy_group,
@@ -24,10 +23,22 @@
static_policy, static_policy,
policy_type, policy_type,
long_predicate long_predicate
</sql>
<select id="findPolicies" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView">
SELECT <include refid="policyColumns"/>
FROM all_policies FROM all_policies
ORDER BY object_owner, object_name, policy_name ORDER BY object_owner, object_name, policy_name
</select> </select>
<select id="findPolicy" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView">
SELECT <include refid="policyColumns"/>
FROM all_policies
WHERE object_owner = UPPER(#{objectOwner,jdbcType=VARCHAR})
AND object_name = UPPER(#{objectName,jdbcType=VARCHAR})
AND policy_name = UPPER(#{policyName,jdbcType=VARCHAR})
</select>
<select id="findFunctionSource" resultType="string"> <select id="findFunctionSource" resultType="string">
SELECT LISTAGG(text, '') WITHIN GROUP (ORDER BY line) SELECT LISTAGG(text, '') WITHIN GROUP (ORDER BY line)
FROM all_source FROM all_source

View File

@@ -0,0 +1,27 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="detail">
<div class="alert alert-warning mb-0" th:if="${errorMessage}" th:text="${errorMessage}"></div>
<div class="probe-exchange-grid" th:if="${detail}">
<section class="probe-exchange">
<h3>Policy Metadata</h3>
<pre th:text="${'Object: ' + detail.policy().objectDisplayName()
+ '\nPolicy Group: ' + detail.policy().policyGroup()
+ '\nPolicy Name: ' + detail.policy().policyName()
+ '\nFunction: ' + detail.policy().functionDisplayName()
+ '\nStatement Types: ' + detail.policy().statementTypes()
+ '\nEnabled: ' + detail.policy().enabled()
+ '\nPolicy Type: ' + detail.policy().policyType()
+ '\nCheck Option: ' + detail.policy().checkOption()
+ '\nStatic Policy: ' + detail.policy().staticPolicy()
+ '\nLong Predicate: ' + detail.policy().longPredicate()}"></pre>
</section>
<section class="probe-exchange">
<h3>DBMS_RLS.ADD_POLICY</h3>
<pre th:text="${detail.ddl()}"></pre>
</section>
</div>
</div>
</body>
</html>

View File

@@ -40,7 +40,15 @@
<strong th:text="${policy.objectDisplayName()}">ADMIN.TABLE</strong> <strong th:text="${policy.objectDisplayName()}">ADMIN.TABLE</strong>
<div class="text-muted small" th:text="${policy.policyGroup()}">SYS_DEFAULT</div> <div class="text-muted small" th:text="${policy.policyGroup()}">SYS_DEFAULT</div>
</td> </td>
<td><code th:text="${policy.policyName()}">POLICY</code></td> <td>
<button class="btn btn-sm btn-outline-secondary policy-function-button"
type="button"
th:hx-get="@{/vpd-policies/policy-detail(objectOwner=${policy.objectOwner()},objectName=${policy.objectName()},policyName=${policy.policyName()})}"
th:hx-target="${'#policy-source-' + iter.index}"
hx-swap="innerHTML">
<code th:text="${policy.policyName()}">POLICY</code>
</button>
</td>
<td> <td>
<button class="btn btn-sm btn-outline-secondary policy-function-button" <button class="btn btn-sm btn-outline-secondary policy-function-button"
type="button" type="button"
@@ -66,7 +74,7 @@
<tr> <tr>
<td colspan="7" class="policy-source-cell"> <td colspan="7" class="policy-source-cell">
<div th:id="${'policy-source-' + iter.index}" class="text-muted small"> <div th:id="${'policy-source-' + iter.index}" class="text-muted small">
Policy function을 클릭하면 소스가 표시됩니다. Policy 또는 policy function을 클릭하면 상세 내역이 표시됩니다.
</div> </div>
</td> </td>
</tr> </tr>