[Developer] #424 show filters and ORDS handler details

Refs #424
This commit is contained in:
devmrko
2026-06-23 14:30:22 +09:00
parent 09e25a2349
commit ae3dd6667e
11 changed files with 197 additions and 3 deletions

View File

@@ -0,0 +1,18 @@
package com.cloudhandson.vpdbackoffice.domain.ords;
public record OrdsHandlerView(
String schemaName,
String moduleName,
String basePath,
String template,
String method,
String sourceType,
String source,
String parameters,
String packageSource
) {
public String fullPath() {
return "/ords/" + schemaName + "/" + basePath + template;
}
}

View File

@@ -8,6 +8,8 @@ public record PermissionView(
String objectName, String objectName,
String action, String action,
String rules, String rules,
String visibleColumns String visibleColumns,
String filterPreview,
String nullPolicyPreview
) { ) {
} }

View File

@@ -0,0 +1,13 @@
package com.cloudhandson.vpdbackoffice.mapper;
import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerView;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OrdsMetadataMapper {
List<OrdsHandlerView> findHandlers();
String findHandlerPackageSource();
}

View File

@@ -0,0 +1,33 @@
package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerView;
import com.cloudhandson.vpdbackoffice.mapper.OrdsMetadataMapper;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
public class OrdsMetadataService {
private final OrdsMetadataMapper mapper;
public OrdsMetadataService(OrdsMetadataMapper mapper) {
this.mapper = mapper;
}
public List<OrdsHandlerView> findHandlers() {
String packageSource = mapper.findHandlerPackageSource();
return mapper.findHandlers().stream()
.map(handler -> new OrdsHandlerView(
handler.schemaName(),
handler.moduleName(),
handler.basePath(),
handler.template(),
handler.method(),
handler.sourceType(),
handler.source(),
handler.parameters(),
packageSource
))
.toList();
}
}

View File

@@ -0,0 +1,22 @@
package com.cloudhandson.vpdbackoffice.web;
import com.cloudhandson.vpdbackoffice.service.OrdsMetadataService;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class OrdsHandlerController {
private final OrdsMetadataService ordsMetadataService;
public OrdsHandlerController(OrdsMetadataService ordsMetadataService) {
this.ordsMetadataService = ordsMetadataService;
}
@GetMapping("/ords-handlers")
public String handlers(Model model) {
model.addAttribute("handlers", ordsMetadataService.findHandlers());
return "ords-handlers";
}
}

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.cloudhandson.vpdbackoffice.mapper.OrdsMetadataMapper">
<select id="findHandlers" resultType="com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerView">
SELECT s.pattern AS schema_name,
m.name AS module_name,
m.uri_prefix AS base_path,
t.uri_template AS template,
h.method,
h.source_type,
DBMS_LOB.SUBSTR(h.source, 12000, 1) AS source,
(
SELECT LISTAGG(p.name || ' -> :' || p.bind_variable_name || ' (' || p.source_type || ')', CHR(10))
WITHIN GROUP (ORDER BY p.name)
FROM dba_ords_parameters p
WHERE p.handler_id = h.id
) AS parameters,
CAST(NULL AS VARCHAR2(1)) AS package_source
FROM dba_ords_schemas s
JOIN dba_ords_modules m ON m.schema_id = s.id
JOIN dba_ords_templates t ON t.module_id = m.id
JOIN dba_ords_handlers h ON h.template_id = t.id
WHERE s.parsing_schema = 'CB_ORDS'
ORDER BY m.name, t.uri_template, h.method
</select>
<select id="findHandlerPackageSource" resultType="string">
SELECT LISTAGG(text, '') WITHIN GROUP (ORDER BY line)
FROM all_source
WHERE owner = 'CB_ORDS'
AND name = 'CB_ORDS_HANDLER_PKG'
AND type = 'PACKAGE BODY'
</select>
</mapper>

View File

@@ -46,7 +46,28 @@
SELECT LISTAGG(pc.column_name, ', ') WITHIN GROUP (ORDER BY pc.column_name) SELECT LISTAGG(pc.column_name, ', ') WITHIN GROUP (ORDER BY pc.column_name)
FROM cb_permission_column pc FROM cb_permission_column pc
WHERE pc.permission_id = p.perm_id WHERE pc.permission_id = p.perm_id
) AS visible_columns ) AS visible_columns,
(
SELECT LISTAGG(
CASE
WHEN pr2.rule_type = 'ALL' THEN 'ALL ROWS'
WHEN pr2.rule_type = 'MY_DEPT' THEN pr2.rule_column || ' = SYS_CONTEXT(CB_AGENT_CTX.DEPT_CODE)'
WHEN pr2.rule_type = 'SELF' THEN pr2.rule_column || ' = SYS_CONTEXT(CB_AGENT_CTX.EMP_NO)'
ELSE pr2.rule_column || ' ' || pr2.rule_type || ' ' || pr2.rule_value
END,
CHR(10) || 'AND '
) WITHIN GROUP (ORDER BY pr2.rule_id)
FROM cb_permission_rule pr2
WHERE pr2.perm_id = p.perm_id
) AS filter_preview,
(
SELECT CASE
WHEN COUNT(*) = 0 THEN '모든 민감 컬럼 NULL 처리'
ELSE 'NULL 제외: ' || LISTAGG(pc2.column_name, ', ') WITHIN GROUP (ORDER BY pc2.column_name)
END
FROM cb_permission_column pc2
WHERE pc2.permission_id = p.perm_id
) AS null_policy_preview
FROM cb_permission p FROM cb_permission p
JOIN cb_app_role r ON r.role_id = p.role_id JOIN cb_app_role r ON r.role_id = p.role_id
LEFT JOIN cb_protected_object o ON o.object_name = p.target_name LEFT JOIN cb_protected_object o ON o.object_name = p.target_name

View File

@@ -347,6 +347,14 @@ body {
word-break: break-word; word-break: break-word;
} }
.table-pre {
background: transparent;
color: inherit;
font-size: .78rem;
margin: 0;
white-space: pre-wrap;
}
@media (max-width: 640px) { @media (max-width: 640px) {
.form-grid .span-2 { .form-grid .span-2 {
grid-column: span 1; grid-column: span 1;

View File

@@ -21,6 +21,7 @@
<a class="nav-link" href="/objects">테이블/뷰</a> <a class="nav-link" href="/objects">테이블/뷰</a>
<a class="nav-link" href="/tokens">토큰</a> <a class="nav-link" href="/tokens">토큰</a>
<a class="nav-link" href="/probe">ORDS 검증</a> <a class="nav-link" href="/probe">ORDS 검증</a>
<a class="nav-link" href="/ords-handlers">ORDS 핸들러</a>
<a class="nav-link" href="/settings">설정</a> <a class="nav-link" href="/settings">설정</a>
</div> </div>
<form method="post" action="/logout" class="ms-auto"> <form method="post" action="/logout" class="ms-auto">

View File

@@ -0,0 +1,39 @@
<!doctype html>
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{fragments/layout :: head('ORDS 핸들러')}"></head>
<body>
<nav th:replace="~{fragments/layout :: nav}"></nav>
<main class="container py-4">
<div class="page-title">
<h1>ORDS 핸들러</h1>
<p>Bearer Token을 DB 컨텍스트로 변환하는 ORDS Handler와 PL/SQL 흐름을 확인합니다.</p>
</div>
<section class="content-band" th:each="handler : ${handlers}">
<div class="section-heading">
<h2 th:text="${handler.method() + ' ' + handler.fullPath()}">POST /ords/...</h2>
<span class="badge text-bg-secondary" th:text="${handler.sourceType()}">plsql</span>
</div>
<div class="meta-row">
<span>Schema: <strong th:text="${handler.schemaName()}">CB_ORDS</strong></span>
<span>Module: <strong th:text="${handler.moduleName()}">cb.agent.security</strong></span>
<span>Template: <strong th:text="${handler.template()}">vpd/documents</strong></span>
</div>
<div class="probe-exchange-grid">
<section class="probe-exchange">
<h3>Header Bindings</h3>
<pre th:text="${handler.parameters()} ?: 'No parameters'"></pre>
</section>
<section class="probe-exchange">
<h3>Handler Source</h3>
<pre th:text="${handler.source()}"></pre>
</section>
<section class="probe-exchange span-2">
<h3>Bearer Token -> User Context Package</h3>
<pre th:text="${handler.packageSource()}"></pre>
</section>
</div>
</section>
</main>
</body>
</html>

View File

@@ -79,6 +79,7 @@
<th>테이블/뷰</th> <th>테이블/뷰</th>
<th>Action</th> <th>Action</th>
<th>행 규칙</th> <th>행 규칙</th>
<th>적용 필터</th>
<th>NULL 제외 컬럼</th> <th>NULL 제외 컬럼</th>
<th></th> <th></th>
</tr> </tr>
@@ -90,6 +91,7 @@
<td th:text="${permission.objectName()}">CB_V_SEARCH_DOCUMENTS</td> <td th:text="${permission.objectName()}">CB_V_SEARCH_DOCUMENTS</td>
<td th:text="${permission.action()}">SELECT</td> <td th:text="${permission.action()}">SELECT</td>
<td th:text="${permission.rules()} ?: '-'">ALL</td> <td th:text="${permission.rules()} ?: '-'">ALL</td>
<td><pre class="table-pre" th:text="${permission.filterPreview()} ?: '-'">ALL ROWS</pre></td>
<td th:text="${permission.visibleColumns()} ?: '-'">CONTENTS</td> <td th:text="${permission.visibleColumns()} ?: '-'">CONTENTS</td>
<td> <td>
<form method="post" action="/permissions/delete" class="inline-form"> <form method="post" action="/permissions/delete" class="inline-form">
@@ -100,7 +102,7 @@
</td> </td>
</tr> </tr>
<tr th:if="${#lists.isEmpty(permissions)}"> <tr th:if="${#lists.isEmpty(permissions)}">
<td colspan="7" class="text-muted">등록된 권한이 없습니다.</td> <td colspan="8" class="text-muted">등록된 권한이 없습니다.</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>