refs #707: enforce HMM leave team VPD

This commit is contained in:
devmrko
2026-07-23 13:36:21 +09:00
parent a495e7a962
commit da80041bcb
18 changed files with 1034 additions and 125 deletions

View File

@@ -67,11 +67,14 @@ public record PermissionView(
if (upper.startsWith("STATIC_SQL ")) {
return "정적 SQL 조건: " + rawRule.substring("STATIC_SQL ".length());
}
if (upper.contains(" MY_DEPT")) {
if ("MY_DEPT".equals(upper) || upper.contains(" MY_DEPT")) {
return "내 부서 행";
}
if (upper.contains(" SELF")) {
return "내 사번/소유자";
if ("MANAGED_TEAM".equals(upper) || upper.contains(" MANAGED_TEAM")) {
return "본인 및 직접 보고 팀원";
}
if ("SELF".equals(upper) || upper.contains(" SELF")) {
return "본인 직원 행";
}
if (upper.contains(" TAG ")) {
return "태그 조건: " + rawRule;

View File

@@ -17,6 +17,7 @@ public record VpdFunctionOption(
}
public boolean permissionSystemDefault() {
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName);
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
|| "HMM_LEAVE_VPD_FILTER".equalsIgnoreCase(functionName);
}
}

View File

@@ -13,9 +13,21 @@ public record VpdPolicyFormOptions(
public String defaultPermissionFunctionKey() {
return functions.stream()
.filter(function -> "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(function.functionName()))
.filter(VpdFunctionOption::permissionSystemDefault)
.findFirst()
.map(VpdFunctionOption::value)
.orElse("");
}
public String defaultPermissionFunctionKey(String objectName) {
String preferredFunction = objectName != null
&& objectName.toUpperCase(java.util.Locale.ROOT).startsWith("HMM_LEAVE_")
? "HMM_LEAVE_VPD_FILTER"
: "CB_AGENT_DOC_VPD_FILTER";
return functions.stream()
.filter(function -> preferredFunction.equalsIgnoreCase(function.functionName()))
.findFirst()
.map(VpdFunctionOption::value)
.orElseGet(this::defaultPermissionFunctionKey);
}
}

View File

@@ -26,6 +26,7 @@ public record VpdPolicyView(
}
public boolean permissionSystemDefault() {
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName);
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
|| "HMM_LEAVE_VPD_FILTER".equalsIgnoreCase(functionName);
}
}

View File

@@ -333,18 +333,16 @@ public class BackofficeSchemaService {
);
}
addCountCheck(checks, "VPD Runtime", "CB_AGENT_CTX", "CONTEXT",
"SELECT COUNT(*) FROM all_context WHERE namespace = 'CB_AGENT_CTX'",
"sqlcl에서 17_agent_ords_security_local_vpd_setup.sql 또는 CREATE CONTEXT를 실행하세요.",
addCountCheck(checks, "VPD Runtime", "HMM_ACCESS_CTX", "CONTEXT",
"SELECT COUNT(*) FROM all_context WHERE namespace = 'HMM_ACCESS_CTX'",
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.",
true);
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CTX_PKG", "PACKAGE",
"sqlcl에서 cb_agent_ctx_pkg package를 생성/컴파일하세요.");
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CTX_PKG BODY", "PACKAGE BODY",
"sqlcl에서 cb_agent_ctx_pkg package body를 생성/컴파일하세요.", "CB_AGENT_CTX_PKG");
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_DOC_VPD_FILTER", "FUNCTION",
"sqlcl에서 26_agent_ords_security_dynamic_vpd_filter.sql을 실행하세요.");
addUserObjectCheck(checks, "VPD Runtime", "CB_AGENT_CAN_READ_COLUMN", "FUNCTION",
"sqlcl에서 backoffice support redaction function을 생성/컴파일하세요.");
addUserObjectCheck(checks, "VPD Runtime", "HMM_ACCESS_CTX_PKG", "PACKAGE",
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.");
addUserObjectCheck(checks, "VPD Runtime", "HMM_ACCESS_CTX_PKG BODY", "PACKAGE BODY",
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.", "HMM_ACCESS_CTX_PKG");
addUserObjectCheck(checks, "VPD Runtime", "HMM_LEAVE_VPD_FILTER", "FUNCTION",
"sqlcl에서 72_hmm_leave_team_vpd.sql을 실행하세요.");
addPolicyCheck(checks);
addCountCheck(checks, "ORDS Runtime", ORDS_SCHEMA, "USER",
@@ -360,8 +358,6 @@ public class BackofficeSchemaService {
addAllObjectCheck(checks, "ORDS Runtime", ORDS_SCHEMA, "CB_ORDS_HANDLER_PKG", "PACKAGE",
"CB_ORDS로 접속해 22_agent_ords_security_ords_handler_setup.sql을 실행하세요.");
addGrantCheck(checks, currentUser, "CB_AGENT_CTX_PKG", "EXECUTE");
addGrantCheck(checks, currentUser, "CB_AGENT_CAN_READ_COLUMN", "EXECUTE");
addProtectedObjectGrantCheck(checks);
return new SchemaPreflightView(currentUser, ORDS_SCHEMA, checks, appDdlPreview(), sqlclScript(currentUser));
@@ -597,14 +593,14 @@ public class BackofficeSchemaService {
SELECT COUNT(*)
FROM all_policies
WHERE pf_owner = USER
AND function = 'CB_AGENT_DOC_VPD_FILTER'
AND function = 'HMM_LEAVE_VPD_FILTER'
""");
checks.add(new SchemaPreflightCheck(
"VPD Runtime",
"CB_AGENT_DOC_VPD_FILTER 적용 policy",
"HMM_LEAVE_VPD_FILTER 적용 policy",
"DBMS_RLS POLICY",
count > 0 ? "OK" : "WARN",
count > 0 ? count + "개 policy가 기본 filter를 사용합니다." : "기본 filter가 적용된 TABLE/VIEW가 없습니다.",
count > 0 ? count + " HMM 휴가 policy가 기본 filter를 사용합니다." : "HMM 휴가 filter가 적용된 TABLE/VIEW가 없습니다.",
count > 0 ? "조치 없음" : "운영 > VPD 설정에서 개별 또는 벌크 적용을 실행하세요.",
false
));
@@ -640,9 +636,14 @@ public class BackofficeSchemaService {
}
private void addProtectedObjectGrantCheck(List<SchemaPreflightCheck> checks) {
if (!exists("SELECT COUNT(*) FROM user_tables WHERE table_name = 'CB_PROTECTED_OBJECT'")) {
if (!exists("""
SELECT COUNT(*)
FROM user_objects
WHERE object_name = 'CB_PROTECTED_OBJECT'
AND object_type IN ('TABLE', 'VIEW')
""")) {
checks.add(new SchemaPreflightCheck("Grants", "보호 객체 SELECT grants", "GRANT", "WARN",
"CB_PROTECTED_OBJECT 테이블이 없어 등록 보호 객체 grant를 계산할 수 없습니다.",
"CB_PROTECTED_OBJECT 객체가 없어 등록 보호 객체 grant를 계산할 수 없습니다.",
"지원 테이블 생성/확인 후 다시 점검하세요.", false));
return;
}
@@ -706,26 +707,16 @@ public class BackofficeSchemaService {
private String sqlclScript(String currentUser) {
String owner = currentUser == null || currentUser.isBlank() ? "ADMIN" : currentUser;
return """
-- sqlcl에서 실행할 VPD/ORDS runtime 준비 순서
-- 1. ADMIN 또는 보호 객체 owner로 접속
@sql/adb/17_agent_ords_security_local_vpd_setup.sql
-- sqlcl에서 실행할 HMM VPD runtime 준비 순서
-- 1. ADMIN로 접속
@sql/adb/25_agent_ords_security_backoffice_support.sql
@sql/adb/26_agent_ords_security_dynamic_vpd_filter.sql
@sql/adb/62_kb_aso_masking_backoffice_metadata.sql
@sql/adb/21_agent_ords_security_ords_enable_schema.sql
@sql/adb/72_hmm_leave_team_vpd.sql
-- 2. ORDS parsing schema로 접속
CONNECT cb_ords/<password>@<tns_alias>
@sql/adb/22_agent_ords_security_ords_handler_setup.sql
-- 3. 대표 권한 부여 SQL
-- 2. 비면제 업무 runtime 사용자에 필요한 최소 권한
CONNECT %s/<password>@<tns_alias>
GRANT EXECUTE ON cb_agent_ctx_pkg TO cb_ords;
GRANT SELECT ON <owner>.<table_or_view> TO cb_ords;
-- 4. 마스킹 규칙을 UI에서 컬럼에 연결한 뒤 실행
@sql/adb/64_kb_aso_masking_default_column_rules.sql
@sql/adb/63_kb_aso_masking_rule_runtime.sql
GRANT EXECUTE ON hmm_access_ctx_pkg TO <hmm_runtime_user>;
GRANT SELECT ON hmm_leave_balances TO <hmm_runtime_user>;
GRANT SELECT ON hmm_leave_requests TO <hmm_runtime_user>;
""".formatted(owner.toLowerCase());
}

View File

@@ -23,14 +23,14 @@ import org.springframework.transaction.annotation.Transactional;
public class PermissionService {
private static final Set<String> RULE_TYPES = Set.of(
"ALL", "=", "!=", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG",
"ALL", "=", "!=", "MANAGED_TEAM", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG",
"STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL",
"TOKEN_SUBJECT", "OWN_CONTRACT", "CHANNEL_CONTRACT", "OWN_CUSTOMER", "CHANNEL_CUSTOMER",
"STATIC_SQL");
private static final Set<String> VALUE_REQUIRED_RULE_TYPES = Set.of(
"=", "!=", "DEPT", "EMP_NO", "TAG", "STAKEHOLDER_SELF", "STAKEHOLDER_CHANNEL", "STATIC_SQL");
private static final Set<String> DEFAULT_COLUMN_RULE_TYPES = Set.of(
"MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG", "STATIC_SQL");
"MANAGED_TEAM", "MY_DEPT", "SELF", "DEPT", "EMP_NO", "TAG", "STATIC_SQL");
private static final Set<String> STATIC_SQL_KEYWORDS = Set.of(
"AND", "OR", "NOT", "NULL", "IS", "IN", "LIKE", "BETWEEN", "ESCAPE", "TRUE", "FALSE",
"UPPER", "LOWER", "TRIM", "NVL", "COALESCE", "TO_CHAR", "TO_DATE", "REGEXP_LIKE", "INSTR",

View File

@@ -39,6 +39,8 @@ public class VpdPolicyService {
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";
private static final String HMM_LEAVE_POLICY_NAME = "HMM_LEAVE_SCOPE_POLICY";
private static final String HMM_LEAVE_FILTER_FUNCTION = "HMM_LEAVE_VPD_FILTER";
/*
* DBMS_RLS.ADD_POLICY accepts PL/SQL BOOLEAN arguments. Keep all four
* permitted flag combinations as fixed statements: database object names
@@ -154,8 +156,8 @@ public class VpdPolicyService {
@Transactional
public void saveFilterFunction(String functionOwnerValue, String functionNameValue, String filterPredicateValue) {
String functionName = requiredIdentifier(functionNameValue, "Function name");
if (DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)) {
throw new AppException("기본 동적 권한 필터 " + DEFAULT_PERMISSION_FILTER_FUNCTION
if (isSystemManagedPermissionFilter(functionName)) {
throw new AppException("기본 동적 권한 필터 " + functionName
+ "는 이 화면에서 수정할 수 없습니다. 권한체계는 사용자·그룹·역할·행 접근 규칙 화면에서 변경하세요.");
}
String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class);
@@ -180,15 +182,18 @@ public class VpdPolicyService {
if (objectParts.length != 2) {
throw new AppException("보호할 객체 형식이 올바르지 않습니다: " + objectKey);
}
String functionKey = formOptions().defaultPermissionFunctionKey();
String functionKey = formOptions().defaultPermissionFunctionKey(objectParts[1]);
if (functionKey.isBlank()) {
throw new AppException("기본 동적 권한 필터 " + DEFAULT_PERMISSION_FILTER_FUNCTION
throw new AppException("대상 객체에 사용할 기본 동적 권한 필터"
+ "가 설치되어 있지 않습니다. 운영 상태에서 동적 권한 필터 설치 여부를 확인한 뒤 다시 적용하세요.");
}
String policyName = isHmmLeaveObject(objectParts[1])
? HMM_LEAVE_POLICY_NAME
: COMMON_POLICY_NAME;
createPolicy(new VpdPolicyCreateCommand(
objectParts[0],
objectParts[1],
COMMON_POLICY_NAME,
policyName,
functionKey,
null,
null,
@@ -352,8 +357,8 @@ public class VpdPolicyService {
description = null;
}
return description == null || description.isBlank()
? (DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)
? "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다."
? (isSystemManagedPermissionFilter(functionName)
? defaultPermissionFilterDescription(functionName)
: "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다.")
: description;
}
@@ -377,7 +382,7 @@ public class VpdPolicyService {
*/
public String findFilterPredicate(String owner, String packageName, String functionName) {
if (packageName != null && !packageName.isBlank()
|| DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)) {
|| isSystemManagedPermissionFilter(functionName)) {
return "";
}
try {
@@ -493,10 +498,11 @@ public class VpdPolicyService {
) {
List<VpdPolicyTemplateOption> templates = new ArrayList<>();
functions.stream()
.filter(function -> DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(function.functionName()))
.findFirst()
.ifPresent(function -> templates.add(new VpdPolicyTemplateOption(
COMMON_POLICY_NAME,
.filter(VpdFunctionOption::permissionSystemDefault)
.forEach(function -> templates.add(new VpdPolicyTemplateOption(
HMM_LEAVE_FILTER_FUNCTION.equalsIgnoreCase(function.functionName())
? HMM_LEAVE_POLICY_NAME
: COMMON_POLICY_NAME,
function.owner(),
function.packageName(),
function.functionName(),
@@ -746,6 +752,23 @@ public class VpdPolicyService {
return formOptions().defaultPermissionFunctionKey();
}
private boolean isSystemManagedPermissionFilter(String functionName) {
return DEFAULT_PERMISSION_FILTER_FUNCTION.equalsIgnoreCase(functionName)
|| HMM_LEAVE_FILTER_FUNCTION.equalsIgnoreCase(functionName);
}
private boolean isHmmLeaveObject(String objectName) {
return objectName != null
&& objectName.toUpperCase(Locale.ROOT).startsWith("HMM_LEAVE_");
}
private String defaultPermissionFilterDescription(String functionName) {
if (HMM_LEAVE_FILTER_FUNCTION.equalsIgnoreCase(functionName)) {
return "HMM 직원 본인·직접 보고 팀원·HR 관리자 역할을 행 조건으로 변환합니다.";
}
return "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다.";
}
private String normalizeStatementTypes(String value) {
String raw = value == null || value.isBlank() ? "SELECT" : value;
List<String> statements = List.of(raw.split(",")).stream()

View File

@@ -153,9 +153,13 @@ public class VpdPolicyController {
}
private static String defaultFilterDescription(String functionName) {
return "CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)
? "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다."
: "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다.";
if ("HMM_LEAVE_VPD_FILTER".equalsIgnoreCase(functionName)) {
return "HMM 직원 본인·직접 보고 팀원·HR 관리자 역할을 행 조건으로 변환합니다.";
}
if ("CB_AGENT_DOC_VPD_FILTER".equalsIgnoreCase(functionName)) {
return "사용자·그룹·역할·TAG 권한을 동적으로 합쳐 VPD predicate를 반환합니다.";
}
return "이 Filter function이 반환하는 predicate로 조회 행을 제한합니다.";
}
private static long elapsedMillis(long started, long finished) {