fix #477: list vpd target objects
This commit is contained in:
@@ -22,4 +22,5 @@
|
|||||||
- 권한 관리 화면은 Backoffice Tables 레이어를 강조한다.
|
- 권한 관리 화면은 Backoffice Tables 레이어를 강조한다.
|
||||||
- VPD 설정 화면은 Oracle Database VPD 레이어를 강조한다.
|
- VPD 설정 화면은 Oracle Database VPD 레이어를 강조한다.
|
||||||
- ORDS Handler 생성과 ORDS 검증 화면은 ORDS 서빙/검증 레이어를 강조한다.
|
- ORDS Handler 생성과 ORDS 검증 화면은 ORDS 서빙/검증 레이어를 강조한다.
|
||||||
- VPD 적용 대상은 TABLE/VIEW로 표기하고, ORDS path는 서빙/검증 화면에서만 노출한다.
|
- VPD 적용 대상은 Oracle DB catalog 기준의 TABLE/VIEW 목록으로 표시한다.
|
||||||
|
- VPD 화면에서는 VPD 적용 여부를 중심으로 보여주고, 백오피스 권한 테이블 등록 여부와 ORDS Path는 서빙 상태 참고 정보로 함께 표시한다.
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.cloudhandson.vpdbackoffice.config;
|
|||||||
|
|
||||||
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
import com.cloudhandson.vpdbackoffice.service.PermissionService;
|
||||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||||
|
import com.cloudhandson.vpdbackoffice.service.VpdPolicyService;
|
||||||
import java.sql.Connection;
|
import java.sql.Connection;
|
||||||
import javax.sql.DataSource;
|
import javax.sql.DataSource;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
@@ -17,15 +18,18 @@ public class DbPoolWarmup {
|
|||||||
private final DataSource dataSource;
|
private final DataSource dataSource;
|
||||||
private final ProtectedObjectService protectedObjectService;
|
private final ProtectedObjectService protectedObjectService;
|
||||||
private final PermissionService permissionService;
|
private final PermissionService permissionService;
|
||||||
|
private final VpdPolicyService vpdPolicyService;
|
||||||
|
|
||||||
public DbPoolWarmup(
|
public DbPoolWarmup(
|
||||||
DataSource dataSource,
|
DataSource dataSource,
|
||||||
ProtectedObjectService protectedObjectService,
|
ProtectedObjectService protectedObjectService,
|
||||||
PermissionService permissionService
|
PermissionService permissionService,
|
||||||
|
VpdPolicyService vpdPolicyService
|
||||||
) {
|
) {
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.protectedObjectService = protectedObjectService;
|
this.protectedObjectService = protectedObjectService;
|
||||||
this.permissionService = permissionService;
|
this.permissionService = permissionService;
|
||||||
|
this.vpdPolicyService = vpdPolicyService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
@EventListener(ApplicationReadyEvent.class)
|
||||||
@@ -45,6 +49,8 @@ public class DbPoolWarmup {
|
|||||||
protectedObjectService.findDatabaseObjects();
|
protectedObjectService.findDatabaseObjects();
|
||||||
permissionService.findRoles();
|
permissionService.findRoles();
|
||||||
permissionService.findPermissionViews();
|
permissionService.findPermissionViews();
|
||||||
|
vpdPolicyService.findVpdTargets();
|
||||||
|
vpdPolicyService.formOptions();
|
||||||
log.info("Backoffice DB catalog cache warmed up in {}ms", (System.nanoTime() - started) / 1_000_000);
|
log.info("Backoffice DB catalog cache warmed up in {}ms", (System.nanoTime() - started) / 1_000_000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.cloudhandson.vpdbackoffice.domain.vpd;
|
||||||
|
|
||||||
|
public record VpdTargetView(
|
||||||
|
String owner,
|
||||||
|
String objectName,
|
||||||
|
String objectType,
|
||||||
|
String protectedYn,
|
||||||
|
String ordsPath,
|
||||||
|
int policyCount,
|
||||||
|
String policyNames
|
||||||
|
) {
|
||||||
|
|
||||||
|
public String objectDisplayName() {
|
||||||
|
return owner + "." + objectName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean protectedObject() {
|
||||||
|
return "Y".equalsIgnoreCase(protectedYn);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean vpdApplied() {
|
||||||
|
return policyCount > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package com.cloudhandson.vpdbackoffice.mapper;
|
|||||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionOption;
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionOption;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdSchemaObjectOption;
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdSchemaObjectOption;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView;
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyView;
|
||||||
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdTargetView;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
@@ -12,6 +13,8 @@ public interface VpdPolicyMapper {
|
|||||||
|
|
||||||
List<VpdPolicyView> findPolicies();
|
List<VpdPolicyView> findPolicies();
|
||||||
|
|
||||||
|
List<VpdTargetView> findVpdTargets();
|
||||||
|
|
||||||
List<String> findPolicyNameOptions();
|
List<String> findPolicyNameOptions();
|
||||||
|
|
||||||
List<String> findSchemaOwnerOptions();
|
List<String> findSchemaOwnerOptions();
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyExplanation;
|
|||||||
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdPolicyFormOptions;
|
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.domain.vpd.VpdSchemaObjectOption;
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdSchemaObjectOption;
|
||||||
|
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdTargetView;
|
||||||
import com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper;
|
import com.cloudhandson.vpdbackoffice.mapper.VpdPolicyMapper;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -21,10 +22,13 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
public class VpdPolicyService {
|
public class VpdPolicyService {
|
||||||
|
|
||||||
private static final Set<String> ALLOWED_STATEMENTS = Set.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX");
|
private static final Set<String> ALLOWED_STATEMENTS = Set.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX");
|
||||||
|
private static final long CATALOG_CACHE_MILLIS = 60_000L;
|
||||||
|
|
||||||
private final VpdPolicyMapper mapper;
|
private final VpdPolicyMapper mapper;
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
private final OpenAiCompatibleClient aiClient;
|
private final OpenAiCompatibleClient aiClient;
|
||||||
|
private volatile CacheEntry<List<VpdTargetView>> vpdTargetsCache;
|
||||||
|
private volatile CacheEntry<VpdPolicyFormOptions> formOptionsCache;
|
||||||
|
|
||||||
public VpdPolicyService(VpdPolicyMapper mapper, JdbcTemplate jdbcTemplate, OpenAiCompatibleClient aiClient) {
|
public VpdPolicyService(VpdPolicyMapper mapper, JdbcTemplate jdbcTemplate, OpenAiCompatibleClient aiClient) {
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
@@ -36,14 +40,30 @@ public class VpdPolicyService {
|
|||||||
return mapper.findPolicies();
|
return mapper.findPolicies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<VpdTargetView> findVpdTargets() {
|
||||||
|
CacheEntry<List<VpdTargetView>> cached = vpdTargetsCache;
|
||||||
|
if (cached != null && !cached.expired()) {
|
||||||
|
return cached.value();
|
||||||
|
}
|
||||||
|
List<VpdTargetView> targets = List.copyOf(mapper.findVpdTargets());
|
||||||
|
vpdTargetsCache = new CacheEntry<>(targets, System.currentTimeMillis() + CATALOG_CACHE_MILLIS);
|
||||||
|
return targets;
|
||||||
|
}
|
||||||
|
|
||||||
public VpdPolicyFormOptions formOptions() {
|
public VpdPolicyFormOptions formOptions() {
|
||||||
return new VpdPolicyFormOptions(
|
CacheEntry<VpdPolicyFormOptions> cached = formOptionsCache;
|
||||||
|
if (cached != null && !cached.expired()) {
|
||||||
|
return cached.value();
|
||||||
|
}
|
||||||
|
VpdPolicyFormOptions options = new VpdPolicyFormOptions(
|
||||||
mapper.findPolicyNameOptions(),
|
mapper.findPolicyNameOptions(),
|
||||||
mapper.findSchemaOwnerOptions(),
|
mapper.findSchemaOwnerOptions(),
|
||||||
mapper.findOwnerOptions(),
|
mapper.findOwnerOptions(),
|
||||||
mapper.findFunctionOptions(),
|
mapper.findFunctionOptions(),
|
||||||
List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX")
|
List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX")
|
||||||
);
|
);
|
||||||
|
formOptionsCache = new CacheEntry<>(options, System.currentTimeMillis() + CATALOG_CACHE_MILLIS);
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
public VpdPolicyFormOptions emptyFormOptions() {
|
public VpdPolicyFormOptions emptyFormOptions() {
|
||||||
@@ -72,6 +92,7 @@ public class VpdPolicyService {
|
|||||||
throw new AppException("Filter predicate는 필수입니다.");
|
throw new AppException("Filter predicate는 필수입니다.");
|
||||||
}
|
}
|
||||||
createFilterFunction(functionName, filterPredicate);
|
createFilterFunction(functionName, filterPredicate);
|
||||||
|
clearCatalogCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -93,6 +114,7 @@ public class VpdPolicyService {
|
|||||||
END;
|
END;
|
||||||
""", objectOwner, objectName, policyName);
|
""", objectOwner, objectName, policyName);
|
||||||
createPolicy(command);
|
createPolicy(command);
|
||||||
|
clearCatalogCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
public VpdBulkApplyResult bulkApplySchema(
|
public VpdBulkApplyResult bulkApplySchema(
|
||||||
@@ -178,6 +200,7 @@ public class VpdPolicyService {
|
|||||||
failed++;
|
failed++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
clearCatalogCache();
|
||||||
return new VpdBulkApplyResult(targets.size(), created, skipped, failed);
|
return new VpdBulkApplyResult(targets.size(), created, skipped, failed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +269,12 @@ public class VpdPolicyService {
|
|||||||
command.enabled(),
|
command.enabled(),
|
||||||
command.updateCheck()
|
command.updateCheck()
|
||||||
);
|
);
|
||||||
|
clearCatalogCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clearCatalogCache() {
|
||||||
|
vpdTargetsCache = null;
|
||||||
|
formOptionsCache = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addPolicy(
|
private void addPolicy(
|
||||||
@@ -496,6 +525,13 @@ public class VpdPolicyService {
|
|||||||
return value.replace("'", "''");
|
return value.replace("'", "''");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record CacheEntry<T>(T value, long expiresAt) {
|
||||||
|
|
||||||
|
boolean expired() {
|
||||||
|
return System.currentTimeMillis() > expiresAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private record FunctionRef(String owner, String packageName, String functionName) {
|
private record FunctionRef(String owner, String packageName, String functionName) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,12 +39,14 @@ public class VpdPolicyController {
|
|||||||
private void populatePolicyModel(Model model) {
|
private void populatePolicyModel(Model model) {
|
||||||
try {
|
try {
|
||||||
model.addAttribute("policies", vpdPolicyService.findPolicies());
|
model.addAttribute("policies", vpdPolicyService.findPolicies());
|
||||||
|
model.addAttribute("vpdTargets", vpdPolicyService.findVpdTargets());
|
||||||
model.addAttribute("objects", protectedObjectService.findEnabled());
|
model.addAttribute("objects", protectedObjectService.findEnabled());
|
||||||
model.addAttribute("formOptions", vpdPolicyService.formOptions());
|
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("vpdTargets", List.of());
|
||||||
model.addAttribute("objects", List.of());
|
model.addAttribute("objects", List.of());
|
||||||
model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions());
|
model.addAttribute("formOptions", vpdPolicyService.emptyFormOptions());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,45 @@
|
|||||||
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="findVpdTargets" resultType="com.cloudhandson.vpdbackoffice.domain.vpd.VpdTargetView">
|
||||||
|
WITH managed_owners AS (
|
||||||
|
SELECT USER AS owner FROM dual
|
||||||
|
UNION
|
||||||
|
SELECT owner FROM cb_protected_object
|
||||||
|
),
|
||||||
|
managed_objects AS (
|
||||||
|
SELECT owner, object_name, object_type
|
||||||
|
FROM all_objects
|
||||||
|
WHERE object_type IN ('TABLE', 'VIEW')
|
||||||
|
AND owner IN (SELECT owner FROM managed_owners)
|
||||||
|
AND owner NOT IN ('SYS', 'SYSTEM', 'ORDS_METADATA', 'ORDS_PUBLIC_USER')
|
||||||
|
AND owner NOT LIKE 'APEX\_%' ESCAPE '\'
|
||||||
|
AND owner NOT LIKE 'C##%'
|
||||||
|
AND object_name NOT LIKE 'BIN$%'
|
||||||
|
)
|
||||||
|
SELECT o.owner,
|
||||||
|
o.object_name,
|
||||||
|
o.object_type,
|
||||||
|
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
|
||||||
|
FROM managed_objects o
|
||||||
|
LEFT JOIN cb_protected_object po
|
||||||
|
ON po.owner = o.owner
|
||||||
|
AND po.object_name = o.object_name
|
||||||
|
AND po.enabled_yn = 'Y'
|
||||||
|
LEFT JOIN all_policies p
|
||||||
|
ON p.object_owner = o.owner
|
||||||
|
AND p.object_name = o.object_name
|
||||||
|
GROUP BY o.owner, o.object_name, o.object_type, po.object_id, po.enabled_yn, po.ords_path
|
||||||
|
ORDER BY CASE WHEN COUNT(p.policy_name) > 0 THEN 0 ELSE 1 END,
|
||||||
|
o.owner,
|
||||||
|
o.object_type,
|
||||||
|
o.object_name
|
||||||
|
FETCH FIRST 500 ROWS ONLY
|
||||||
|
</select>
|
||||||
|
|
||||||
<select id="findPolicyNameOptions" resultType="string">
|
<select id="findPolicyNameOptions" resultType="string">
|
||||||
SELECT DISTINCT policy_name
|
SELECT DISTINCT policy_name
|
||||||
FROM all_policies
|
FROM all_policies
|
||||||
|
|||||||
@@ -305,7 +305,7 @@ body {
|
|||||||
.section-heading h2 {
|
.section-heading h2 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
margin: 0 0 1rem;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
@@ -313,6 +313,13 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-subtitle {
|
||||||
|
color: var(--rw-muted);
|
||||||
|
font-size: .875rem;
|
||||||
|
margin: .2rem 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-grid {
|
.form-grid {
|
||||||
|
|||||||
@@ -26,6 +26,57 @@
|
|||||||
<p class="text-muted mb-0">VPD는 개별 보호 객체 적용을 기본으로 하고, 같은 정책을 스키마 TABLE/VIEW에 확장할 때만 벌크 적용을 사용합니다.</p>
|
<p class="text-muted mb-0">VPD는 개별 보호 객체 적용을 기본으로 하고, 같은 정책을 스키마 TABLE/VIEW에 확장할 때만 벌크 적용을 사용합니다.</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="content-band">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<h2>VPD 적용 대상 TABLE/VIEW</h2>
|
||||||
|
<p class="section-subtitle">Oracle DB catalog 기준의 TABLE/VIEW 목록입니다. ORDS는 이 객체를 HTTP로 서빙하는 별도 레이어입니다.</p>
|
||||||
|
</div>
|
||||||
|
<span class="badge text-bg-secondary" th:text="${#lists.size(vpdTargets)}">0</span>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-sm align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Object</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>VPD 상태</th>
|
||||||
|
<th>Policy</th>
|
||||||
|
<th>백오피스 권한 테이블</th>
|
||||||
|
<th>ORDS Path</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr th:each="target : ${vpdTargets}">
|
||||||
|
<td><code th:text="${target.objectDisplayName()}">ADMIN.TABLE</code></td>
|
||||||
|
<td th:text="${target.objectType()}">TABLE</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge"
|
||||||
|
th:classappend="${target.vpdApplied()} ? ' text-bg-success' : ' text-bg-secondary'"
|
||||||
|
th:text="${target.vpdApplied()} ? 'VPD 적용됨' : '미적용'">미적용</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span th:if="${target.vpdApplied()}" th:text="${target.policyNames()}">POLICY</span>
|
||||||
|
<span th:unless="${target.vpdApplied()}" class="text-muted">-</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge"
|
||||||
|
th:classappend="${target.protectedObject()} ? ' text-bg-primary' : ' text-bg-light'"
|
||||||
|
th:text="${target.protectedObject()} ? '등록됨' : '미등록'">미등록</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code th:if="${target.ordsPath()}" th:text="${target.ordsPath()}">cb-ords/path</code>
|
||||||
|
<span th:unless="${target.ordsPath()}" class="text-muted">ORDS 서빙 미등록</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr th:if="${#lists.isEmpty(vpdTargets)}">
|
||||||
|
<td colspan="6" class="text-muted">조회 가능한 TABLE/VIEW가 없습니다. DB 연결 사용자 권한과 스키마 객체를 확인하세요.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="content-band">
|
<section class="content-band">
|
||||||
<div class="section-heading">
|
<div class="section-heading">
|
||||||
<h2>개별 적용</h2>
|
<h2>개별 적용</h2>
|
||||||
|
|||||||
Reference in New Issue
Block a user