Consolidate data access control backoffice updates

This commit is contained in:
devmrko
2026-07-13 23:06:23 +09:00
parent 403298d474
commit e18b30feab
181 changed files with 11571 additions and 954 deletions

View File

@@ -1,6 +1,7 @@
package com.cloudhandson.vpdbackoffice.service;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdBulkApplyResult;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdDescriptionNote;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionOption;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdFunctionSource;
import com.cloudhandson.vpdbackoffice.domain.vpd.VpdObjectFilterDetail;
@@ -20,6 +21,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.dao.DataAccessException;
@@ -31,9 +33,54 @@ import org.springframework.transaction.annotation.Transactional;
public class VpdPolicyService {
private static final Set<String> ALLOWED_STATEMENTS = Set.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX");
private static final long CATALOG_CACHE_MILLIS = 60_000L;
// ALL_* dictionary views are comparatively expensive in Autonomous Database.
// Mutating VPD operations call clearCatalogCache(), so a longer read cache does
// not delay an administrator's own changes from appearing in the UI.
private static final long CATALOG_CACHE_MILLIS = 15 * 60_000L;
private static final String COMMON_POLICY_NAME = "CB_PERMISSION_SELECT_POLICY";
private static final String DEFAULT_PERMISSION_FILTER_FUNCTION = "CB_AGENT_DOC_VPD_FILTER";
/*
* DBMS_RLS.ADD_POLICY accepts PL/SQL BOOLEAN arguments. Keep all four
* permitted flag combinations as fixed statements: database object names
* and function references are JDBC bind values, and no request value is
* ever interpolated into executable SQL or PL/SQL source.
*/
private static final String ADD_POLICY_ENABLED_WITH_UPDATE_CHECK = """
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => ?, object_name => ?, policy_name => ?,
function_schema => ?, policy_function => ?, statement_types => ?,
update_check => TRUE, enable => TRUE, policy_type => DBMS_RLS.DYNAMIC
);
END;
""";
private static final String ADD_POLICY_ENABLED_WITHOUT_UPDATE_CHECK = """
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => ?, object_name => ?, policy_name => ?,
function_schema => ?, policy_function => ?, statement_types => ?,
update_check => FALSE, enable => TRUE, policy_type => DBMS_RLS.DYNAMIC
);
END;
""";
private static final String ADD_POLICY_DISABLED_WITH_UPDATE_CHECK = """
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => ?, object_name => ?, policy_name => ?,
function_schema => ?, policy_function => ?, statement_types => ?,
update_check => TRUE, enable => FALSE, policy_type => DBMS_RLS.DYNAMIC
);
END;
""";
private static final String ADD_POLICY_DISABLED_WITHOUT_UPDATE_CHECK = """
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => ?, object_name => ?, policy_name => ?,
function_schema => ?, policy_function => ?, statement_types => ?,
update_check => FALSE, enable => FALSE, policy_type => DBMS_RLS.DYNAMIC
);
END;
""";
private static final Pattern RETURN_LITERAL = Pattern.compile(
"(?is)\\bRETURN\\s+'((?:''|[^'])*)'\\s*;"
);
@@ -42,7 +89,8 @@ public class VpdPolicyService {
private final JdbcTemplate jdbcTemplate;
private final OpenAiCompatibleClient aiClient;
private final Map<String, CacheEntry<List<VpdTargetView>>> vpdTargetsCache = new ConcurrentHashMap<>();
private volatile CacheEntry<VpdPolicyFormOptions> formOptionsCache;
private final AtomicReference<CacheEntry<VpdPolicyFormOptions>> formOptionsCache =
new AtomicReference<>();
public VpdPolicyService(VpdPolicyMapper mapper, JdbcTemplate jdbcTemplate, OpenAiCompatibleClient aiClient) {
this.mapper = mapper;
@@ -71,7 +119,7 @@ public class VpdPolicyService {
}
public VpdPolicyFormOptions formOptions() {
CacheEntry<VpdPolicyFormOptions> cached = formOptionsCache;
CacheEntry<VpdPolicyFormOptions> cached = formOptionsCache.get();
if (cached != null && !cached.expired()) {
return cached.value();
}
@@ -84,7 +132,7 @@ public class VpdPolicyService {
buildPolicyTemplateOptions(functions, mapper.findPolicyTemplateOptions()),
List.of("SELECT", "INSERT", "UPDATE", "DELETE", "INDEX")
);
formOptionsCache = new CacheEntry<>(options, System.currentTimeMillis() + CATALOG_CACHE_MILLIS);
formOptionsCache.set(new CacheEntry<>(options, System.currentTimeMillis() + CATALOG_CACHE_MILLIS));
return options;
}
@@ -108,7 +156,7 @@ public class VpdPolicyService {
String functionName = requiredIdentifier(functionNameValue, "Function name");
if (DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)) {
throw new AppException("기본 동적 권한 필터 " + DEFAULT_PERMISSION_FILTER_FUNCTION
+ "는 이 화면에서 수정할 수 없습니다. 권한체계는 사용자·그룹·역할·권한 규칙 화면에서 변경하세요.");
+ "는 이 화면에서 수정할 수 없습니다. 권한체계는 사용자·그룹·역할·행 접근 규칙 화면에서 변경하세요.");
}
String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class);
String functionOwner = functionOwnerValue == null || functionOwnerValue.isBlank()
@@ -292,7 +340,7 @@ public class VpdPolicyService {
description = null;
}
return description == null || description.isBlank()
? objectOwner + "." + objectName + "에 요청마다 현재 권한체계의 행 접근 조건을 적용하는 " + policyName + " policy입니다."
? objectOwner + "." + objectName + "에 요청마다 현재 행 접근 규칙의 조건을 적용하는 " + policyName + " policy입니다."
: description;
}
@@ -310,6 +358,18 @@ public class VpdPolicyService {
: description;
}
/**
* Retrieves all UI descriptions in one round trip. The policy screen used to
* issue one ADB query for every displayed policy and filter.
*/
public Map<String, String> findPolicyDescriptionMap() {
return descriptionMap(mapper.findPolicyDescriptions());
}
public Map<String, String> findFilterDescriptionMap() {
return descriptionMap(mapper.findFilterDescriptions());
}
/**
* Returns the literal predicate used by a simple standalone Filter function created by this UI.
* Packaged or system-managed functions intentionally return an empty string because their
@@ -414,7 +474,17 @@ public class VpdPolicyService {
public void clearCatalogCache() {
vpdTargetsCache.clear();
formOptionsCache = null;
formOptionsCache.set(null);
}
private Map<String, String> descriptionMap(List<VpdDescriptionNote> notes) {
Map<String, String> descriptions = new LinkedHashMap<>();
for (VpdDescriptionNote note : notes) {
if (note.noteKey() != null && note.description() != null && !note.description().isBlank()) {
descriptions.put(note.noteKey(), note.description());
}
}
return descriptions;
}
private List<VpdPolicyTemplateOption> buildPolicyTemplateOptions(
@@ -453,21 +523,7 @@ public class VpdPolicyService {
boolean enabled,
boolean updateCheck
) {
jdbcTemplate.update("""
BEGIN
DBMS_RLS.ADD_POLICY(
object_schema => ?,
object_name => ?,
policy_name => ?,
function_schema => ?,
policy_function => ?,
statement_types => ?,
update_check => %s,
enable => %s,
policy_type => DBMS_RLS.DYNAMIC
);
END;
""".formatted(updateCheck ? "TRUE" : "FALSE", enabled ? "TRUE" : "FALSE"),
jdbcTemplate.update(addPolicySql(enabled, updateCheck),
objectOwner,
objectName,
policyName,
@@ -476,6 +532,17 @@ public class VpdPolicyService {
statementTypes);
}
private String addPolicySql(boolean enabled, boolean updateCheck) {
if (enabled) {
return updateCheck
? ADD_POLICY_ENABLED_WITH_UPDATE_CHECK
: ADD_POLICY_ENABLED_WITHOUT_UPDATE_CHECK;
}
return updateCheck
? ADD_POLICY_DISABLED_WITH_UPDATE_CHECK
: ADD_POLICY_DISABLED_WITHOUT_UPDATE_CHECK;
}
public VpdPolicyExplanation explainPolicy(String objectOwner, String objectName, String policyName) {
VpdPolicyDetail detail = findPolicyDetail(objectOwner, objectName, policyName);
VpdPolicyView policy = detail.policy();
@@ -538,7 +605,7 @@ public class VpdPolicyService {
## 요약
- 이 policy가 무엇을 허용/차단하는지 3줄 이내로 먼저 설명한다.
- fail-closed 조건이 있으면 요약에 포함한다.
- 컬럼 마스킹/NULL 처리 판단 가능 여부를 요약에 포함한다.
- 컬럼 마스킹은 ASO/Data Redaction 별도 정책에서 판단한다. 이 VPD source만으로 알 수 있는 행 접근 범위와, 컬럼 마스킹 판단 가능 여부를 구분한다.
## 상세
@@ -557,7 +624,7 @@ public class VpdPolicyService {
### 4. 실제 접근 결과 해석
- 이 policy가 행(row)을 허용하는 조건과 제외하는 조건을 구분한다.
- 컬럼 마스킹/NULL 처리는 source에 직접 있지 않으면 "이 policy source만으로는 판단 불가"라고 쓴다.
- 컬럼 마스킹은 ASO/Data Redaction 별도 정책이다. source에 직접 있지 않으면 " VPD policy source만으로는 컬럼 마스킹 판단 불가"라고 쓴다.
### 5. 운영 확인 포인트
- 운영자가 DB에서 확인할 테이블/컬럼/컨텍스트 값을 5개 이하로 적는다.
@@ -628,6 +695,14 @@ public class VpdPolicyService {
}
private void createFilterFunction(String functionName, String filterPredicate) {
/*
* Oracle DDL cannot bind an object identifier or a function body. This
* is therefore deliberately the sole dynamic-DDL boundary in this
* service. functionName passed here has already gone through
* requiredIdentifier() ([A-Z][A-Z0-9_$#]{0,127}); filterPredicate is put
* inside one SQL literal after every quote is doubled. Those two checks
* prevent a caller from terminating the statement or adding DDL.
*/
jdbcTemplate.execute("""
CREATE OR REPLACE FUNCTION %s(
p_schema_name IN VARCHAR2,