fix #477: show target policy filters

This commit is contained in:
devmrko
2026-06-26 06:21:29 +09:00
parent 7fe8d14a40
commit d010f2659a
9 changed files with 132 additions and 4 deletions

View File

@@ -0,0 +1,20 @@
package com.cloudhandson.vpdbackoffice.domain.vpd;
import java.util.List;
public record VpdObjectFilterDetail(
String objectOwner,
String objectName,
List<Row> rows
) {
public String objectDisplayName() {
return objectOwner + "." + objectName;
}
public record Row(
VpdPolicyView policy,
VpdFunctionSource source
) {
}
}

View File

@@ -7,7 +7,8 @@ public record VpdTargetView(
String protectedYn,
String ordsPath,
int policyCount,
String policyNames
String policyNames,
String filterNames
) {
public String objectDisplayName() {

View File

@@ -35,6 +35,11 @@ public interface VpdPolicyMapper {
@Param("policyName") String policyName
);
List<VpdPolicyView> findPoliciesForObject(
@Param("objectOwner") String objectOwner,
@Param("objectName") String objectName
);
VpdPolicyView findAnyPolicy(
@Param("objectOwner") String objectOwner,
@Param("objectName") String objectName,

View File

@@ -2,6 +2,7 @@ package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdBulkApplyResult;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionSource;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdObjectFilterDetail;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyCreateCommand;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyDetail;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyExplanation;
@@ -233,6 +234,18 @@ public class VpdPolicyService {
return new VpdPolicyDetail(policy, buildAddPolicyBlock(policy));
}
public VpdObjectFilterDetail findObjectFilterDetail(String objectOwner, String objectName) {
String normalizedOwner = requiredIdentifier(objectOwner, "Object owner");
String normalizedObject = requiredIdentifier(objectName, "Object name");
List<VpdObjectFilterDetail.Row> rows = mapper.findPoliciesForObject(normalizedOwner, normalizedObject).stream()
.map(policy -> new VpdObjectFilterDetail.Row(
policy,
findFunctionSource(policy.functionOwner(), policy.packageName(), policy.functionName())
))
.toList();
return new VpdObjectFilterDetail(normalizedOwner, normalizedObject, rows);
}
@Transactional
public void createPolicy(VpdPolicyCreateCommand command) {
String objectOwner = requiredIdentifier(command.objectOwner(), "Object owner");

View File

@@ -306,6 +306,23 @@ public class VpdPolicyController {
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,

View File

@@ -63,7 +63,13 @@
CASE WHEN po.object_id IS NULL THEN 'N' ELSE po.enabled_yn END AS protected_yn,
po.ords_path,
COUNT(p.policy_name) AS policy_count,
LISTAGG(p.policy_name, ', ') WITHIN GROUP (ORDER BY p.policy_name) AS policy_names
LISTAGG(p.policy_name, ', ') WITHIN GROUP (ORDER BY p.policy_name) AS policy_names,
LISTAGG(
p.pf_owner ||
CASE WHEN p.package IS NULL THEN '.' ELSE '.' || p.package || '.' END ||
p.function,
', '
) WITHIN GROUP (ORDER BY p.policy_name) AS filter_names
FROM managed_objects o
LEFT JOIN cb_protected_object po
ON po.owner = o.owner
@@ -210,6 +216,14 @@
)
</select>
<select id="findPoliciesForObject" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView">
SELECT <include refid="policyColumns"><property name="alias" value="p."/></include>
FROM all_policies p
WHERE p.object_owner = UPPER(#{objectOwner,jdbcType=VARCHAR})
AND p.object_name = UPPER(#{objectName,jdbcType=VARCHAR})
ORDER BY p.policy_name
</select>
<select id="findAnyPolicy" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView">
SELECT <include refid="policyColumns"><property name="alias" value="p."/></include>
FROM all_policies p

View File

@@ -870,6 +870,12 @@ body {
padding: .75rem;
}
.filter-detail-stack + .filter-detail-stack {
border-top: 1px solid var(--rw-border);
margin-top: 1rem;
padding-top: 1rem;
}
.policy-detail-list {
display: grid;
grid-template-columns: minmax(120px, 34%) 1fr;

View File

@@ -0,0 +1,31 @@
<!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>
<section class="probe-exchange" th:if="${detail}">
<h3>
<span th:text="${detail.objectDisplayName()}">ADMIN.TABLE</span>
<span class="text-muted"> / Policy Filter</span>
</h3>
<div th:if="${#lists.isEmpty(detail.rows())}" class="text-muted">
이 TABLE/VIEW에 적용된 VPD policy/filter가 없습니다.
</div>
<div class="filter-detail-stack" th:each="row : ${detail.rows()}">
<dl class="policy-detail-list">
<dt>Policy</dt>
<dd><code th:text="${row.policy().policyName()}">POLICY</code></dd>
<dt>Filter</dt>
<dd><code th:text="${row.policy().functionDisplayName()}">ADMIN.FILTER</code></dd>
<dt>Statements</dt>
<dd th:text="${row.policy().statementTypes()} ?: '-'">SELECT</dd>
<dt>Status</dt>
<dd th:text="${row.policy().enabled()}">YES</dd>
</dl>
<pre th:if="${row.source().found()}" th:text="${row.source().source()}"></pre>
<pre th:unless="${row.source().found()}">ALL_SOURCE에서 조회 가능한 filter source가 없습니다.</pre>
</div>
</section>
</div>
</body>
</html>

View File

@@ -54,12 +54,14 @@
<th>Type</th>
<th>VPD 상태</th>
<th>Policy</th>
<th>Filter</th>
<th>백오피스 권한 테이블</th>
<th>ORDS Path</th>
</tr>
</thead>
<tbody>
<tr th:each="target : ${vpdTargets}">
<th:block th:each="target, iter : ${vpdTargets}">
<tr>
<td><code th:text="${target.objectDisplayName()}">ADMIN.TABLE</code></td>
<td th:text="${target.objectType()}">TABLE</td>
<td>
@@ -71,6 +73,17 @@
<span th:if="${target.vpdApplied()}" th:text="${target.policyNames()}">POLICY</span>
<span th:unless="${target.vpdApplied()}" class="text-muted">-</span>
</td>
<td>
<button th:if="${target.vpdApplied()}"
class="btn btn-sm btn-outline-secondary policy-function-button"
type="button"
th:hx-get="@{/vpd-policies/object-filter-detail(objectOwner=${target.owner()},objectName=${target.objectName()})}"
th:hx-target="${'#target-filter-detail-' + iter.index}"
hx-swap="innerHTML">
<code th:text="${target.filterNames()}">ADMIN.FILTER</code>
</button>
<span th:unless="${target.vpdApplied()}" class="text-muted">-</span>
</td>
<td>
<span class="badge"
th:classappend="${target.protectedObject()} ? ' text-bg-primary' : ' text-bg-light'"
@@ -81,8 +94,16 @@
<span th:unless="${target.ordsPath()}" class="text-muted">ORDS 서빙 미등록</span>
</td>
</tr>
<tr th:if="${target.vpdApplied()}">
<td colspan="7" class="policy-source-cell">
<div th:id="${'target-filter-detail-' + iter.index}" class="text-muted small">
Filter 이름을 클릭하면 policy/filter source가 표시됩니다.
</div>
</td>
</tr>
</th:block>
<tr th:if="${#lists.isEmpty(vpdTargets)}">
<td colspan="6" class="text-muted">
<td colspan="7" class="text-muted">
<span th:if="${selectedSchemaOwner != null && selectedSchemaOwner != ''}"
th:text="${selectedSchemaOwner + ' 스키마에서 현재 DB 연결 사용자로 조회 가능한 TABLE/VIEW가 없습니다. 스키마명, ALL_OBJECTS 조회 권한, 객체 권한을 확인하세요.'}">
선택한 스키마에서 조회 가능한 TABLE/VIEW가 없습니다.