Consolidate data access control backoffice updates
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user